//! a container element that can hold and layout multiple children elements

use derive_setters::Setters;
use glam::{Vec2, vec2};
use crate::{
  element::{ElementList, MeasureContext, ProcessContext, UiElement},
  layout::{Alignment, Alignment2d, Direction, LayoutInfo, Size, Size2d},
  frame::{Frame, FrameRect},
  measure::{Hints, Response},
  rect::{Sides, FillColor},
};

//XXX: add Order/Direction::Forward/Reverse or sth?
//TODO: clip children flag
//TODO: borders
//TODO: min/max size

#[derive(Clone, Copy)]
struct CudLine {
  start_idx: usize,
  content_size: Vec2,
}

struct ContainerUserData {
  lines: Vec<CudLine>,
}

/// A container element that can hold and layout multiple children elements
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct Container {
  /// Size of the container
  #[setters(into)]
  pub size: Size2d,

  /// Layout direction (horizontal/vertical)
  pub direction: Direction,

  //XXX: should we have separate gap value for primary and secondary (when wrapped, between lines of elements) axis?

  /// Gap between children elements
  pub gap: f32,

  /// Padding inside the container (distance from the edges to the children elements)
  #[setters(into)]
  pub padding: Sides<f32>,

  /// Alignment of the children elements on X and Y axis
  #[setters(into)]
  pub align: Alignment2d,

  #[setters(skip)]
  pub background_frame: Box<dyn Frame>,

  /// Set this to `true` to allow the elements wrap automatically
  ///
  /// Disabling/enabling this does not affect explicit wrapping\
  /// (for example, `Br`, or any other element with `should_wrap` set to `true`)
  ///
  /// This is an experimental feature and may not work as expected
  pub wrap: bool,

  /// List of children elements
  #[setters(skip)]
  pub children: ElementList,
}

impl Container {
  pub fn with_children(mut self, ui: impl FnOnce(&mut ElementList)) -> Self {
    self.children.0.extend(ElementList::from_callback(ui).0);
    self
  }

  pub fn with_background(mut self, frame: impl Frame + 'static) -> Self {
    self.background_frame = Box::new(frame);
    self
  }
}

impl Default for Container {
  fn default() -> Self {
    Self {
      size: (Size::Auto, Size::Auto).into(),
      direction: Direction::Vertical,
      gap: 0.,
      padding: Sides::all(0.),
      align: Alignment2d::default(),
      background_frame: Box::<FrameRect>::default(),
      wrap: false,
      children: ElementList(Vec::new()),
    }
  }
}

impl Container {
  pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 {
    let outer_size_x = match self.size.width {
      Size::Auto => layout.max_size.x,
      Size::Relative(p) => layout.max_size.x * p,
      Size::Absolute(p) => p,
    };
    let outer_size_y = match self.size.height {
      Size::Auto => layout.max_size.y,
      Size::Relative(p) => layout.max_size.y * p,
      Size::Absolute(p) => p,
    };
    vec2(
      outer_size_x - (self.padding.left + self.padding.right),
      outer_size_y - (self.padding.top + self.padding.bottom),
    )
  }
}

impl UiElement for Container {
  fn name(&self) -> &'static str {
    "container"
  }

  fn measure(&self, ctx: MeasureContext) -> Response {
    // XXX: If both axes are NOT set to auto, we should be able quickly return the size
    // ... but we can't, because we need to measure the children to get the inner_content_size and user_data values
    // this is a potential optimization opportunity, maybe we could postpone this to the process call
    // as it's guaranteed to be called only ONCE, while measure is assumed to be cheap and called multiple times
    // ... we could also implement some sort of "global" caching for the measure call (to prevent traversal of the same tree multiple times),
    // but that's a bit more complex and probably impossible with the current design of the measure/process calls

    // In case wrapping is enabled, elements cannot exceed this size on the primary axis
    let max_line_pri = match self.direction {
      Direction::Horizontal => match self.size.width {
        Size::Auto => ctx.layout.max_size.x,
        Size::Relative(p) => ctx.layout.max_size.x * p,
        Size::Absolute(p) => p,
      },
      Direction::Vertical => match self.size.height {
        Size::Auto => ctx.layout.max_size.y,
        Size::Relative(p) => ctx.layout.max_size.y * p,
        Size::Absolute(p) => p,
      }
    };

    //size of AABB containing all lines
    let mut total_size = Vec2::ZERO;

    //Size of the current row/column (if wrapping)
    let mut line_size = Vec2::ZERO;

    //Size of previous sec. axes combined
    //(basically, in case of the horizontal layout, this is the height of the tallest element in the line)
    //This is a vec2, but only one axis is used, depending on the layout direction
    let mut line_sec_offset: Vec2 = Vec2::ZERO;

    //Amount of elements in the current line
    let mut line_element_count = 0;

    //Leftover gap from the previous element on the primary axis
    let mut leftover_gap = Vec2::ZERO;

    //line metadata for the user_data
    let mut lines = vec![
      CudLine {
        start_idx: 0,
        content_size: Vec2::ZERO,
      }
    ];

    for (idx, element) in self.children.0.iter().enumerate() {
      let measure = element.measure(MeasureContext{
        state: ctx.state,
        layout: &LayoutInfo {
          //XXX: if the element gets wrapped, this will be inaccurate.
          //But, we cant know the size of the line until we measure it, and also
          //We dont make any guarantees about this value being valid during the `measure` call
          //For all intents and purposes, this is just a *hint* for the element to use
          //(and could be just set to 0 for all we care)
          position: ctx.layout.position + line_size + line_sec_offset,
          //TODO: subtract size already taken by previous children
          max_size: self.measure_max_inner_size(ctx.layout),
          direction: self.direction,
        },
        text_measure: ctx.text_measure,
        current_font: ctx.current_font,
        images: ctx.images,
      });

      //Check the position of the side of element closest to the end on the primary axis
      let end_pos_pri = match self.direction {
        Direction::Horizontal => line_size.x + measure.size.x + self.padding.left + self.padding.right,
        Direction::Vertical => line_size.y + measure.size.y + self.padding.top + self.padding.bottom,
      };

      //Wrap the element if it exceeds container's size and is not the first element in the line
      if ((self.wrap && (end_pos_pri > max_line_pri)) || measure.should_wrap) && (line_element_count > 0) {
        // >>>>>>> WRAP THAT B*TCH!

        //Negate the leftover gap from the previous element
        line_size -= leftover_gap;

        //update the previous line metadata
        lines.last_mut().unwrap().content_size = line_size;

        //push the line metadata
        lines.push(CudLine {
          start_idx: idx,
          content_size: Vec2::ZERO,
        });

        //Update the total size accordingly
        match self.direction {
          Direction::Horizontal => {
            total_size.x = total_size.x.max(line_size.x);
            total_size.y += line_size.y + self.gap;
          },
          Direction::Vertical => {
            total_size.x += line_size.x + self.gap;
            total_size.y = total_size.y.max(line_size.y);
          }
        }

        //Now, update line_sec_offset
        match self.direction {
          Direction::Horizontal => {
            line_sec_offset.y += measure.size.y + self.gap;
          },
          Direction::Vertical => {
            line_sec_offset.x += measure.size.x + self.gap;
          }
        };

        //Reset the line size and element count
        line_size = Vec2::ZERO;
        line_element_count = 0;
      }

      //Increment element count
      line_element_count += 1;

      //Sset the leftover gap in case this is the last element in the line
      match self.direction {
        Direction::Horizontal => {
          line_size.x += measure.size.x + self.gap;
          line_size.y = line_size.y.max(measure.size.y);
          leftover_gap = vec2(self.gap, 0.);
        },
        Direction::Vertical => {
          line_size.x = line_size.x.max(measure.size.x);
          line_size.y += measure.size.y + self.gap;
          leftover_gap = vec2(0., self.gap);
        }
      }
    }

    line_size -= leftover_gap;

    //Update the content size of the last line
    lines.last_mut().unwrap().content_size = line_size;

    //Update the total size according to the size of the last line
    match self.direction {
      Direction::Horizontal => {
        total_size.x = total_size.x.max(line_size.x);
        total_size.y += line_size.y;
      },
      Direction::Vertical => {
        total_size.x += line_size.x;
        total_size.y = total_size.y.max(line_size.y);
      }
    }

    //Now, total_size should hold the size of the AABB containing all lines
    //This is exactly what inner_content_size hint should be set to
    let inner_content_size = Some(total_size);

    //After setting the inner_content_size, we can calculate the size of the container
    //Including padding, and in case the size is set to non-auto, override the size

    total_size += vec2(
      self.padding.left + self.padding.right,
      self.padding.top + self.padding.bottom,
    );

    match self.size.width {
      Size::Auto => (),
      Size::Relative(percentage) => total_size.x = ctx.layout.max_size.x * percentage,
      Size::Absolute(pixels) => total_size.x = pixels,
    }
    match self.size.height {
      Size::Auto => (),
      Size::Relative(percentage) => total_size.y = ctx.layout.max_size.y * percentage,
      Size::Absolute(pixels) => total_size.y = pixels,
    }

    Response {
      size: total_size,
      hints: Hints {
        inner_content_size,
        ..Default::default()
      },
      user_data: Some(Box::new(ContainerUserData { lines })),
      ..Default::default()
    }
  }

  fn process(&self, ctx: ProcessContext) {
    let user_data: &ContainerUserData = ctx.measure.user_data
      .as_ref().expect("no user data attached to container")
      .downcast_ref().expect("invalid user data type");

    let mut position = ctx.layout.position;

    //background
    // if !self.background.is_transparent() {
    //   let corner_colors = self.background.corners();
    //   ctx.draw.add(UiDrawCommand::Rectangle {
    //     position,
    //     size: ctx.measure.size,
    //     color: corner_colors,
    //     texture: self.background_image,
    //     rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
    //       RoundedCorners::from_radius(self.corner_radius)
    //     }),
    //   });
    // }

    self.background_frame.draw(ctx.draw, ctx.layout.position, ctx.measure.size);

    //padding
    position += vec2(self.padding.left, self.padding.top);

    //convert alignment to pri/sec axis based
    //.0 = primary, .1 = secondary
    let pri_sec_align = match self.direction {
      Direction::Horizontal => (self.align.horizontal, self.align.vertical),
      Direction::Vertical => (self.align.vertical, self.align.horizontal),
    };

    //alignment (on sec. axis)
    // match pri_sec_align.1 {
    //   Alignment::Begin => (),
    //   Alignment::Center => {
    //     position += match self.direction {
    //       UiDirection::Horizontal => vec2(0., (ctx.measure.size.y - self.padding.top - self.padding.bottom - user_data.lines.last().unwrap().content_size.y) / 2.),
    //       UiDirection::Vertical => vec2((ctx.measure.size.x - self.padding.left - self.padding.right - user_data.lines.last().unwrap().content_size.x) / 2., 0.),
    //     };
    //   },
    //   Alignment::End => {
    //     position += match self.direction {
    //       UiDirection::Horizontal => vec2(0., ctx.measure.size.y - user_data.lines.last().unwrap().content_size.y - self.padding.bottom - self.padding.top),
    //       UiDirection::Vertical => vec2(ctx.measure.size.x - user_data.lines.last().unwrap().content_size.x - self.padding.right - self.padding.left, 0.),
    //     };
    //   }
    // }

    for (line_idx, cur_line) in user_data.lines.iter().enumerate() {
      let mut local_position = position;

      //alignment on primary axis
      match (pri_sec_align.0, self.direction) {
        (Alignment::Begin, _) => (),
        (Alignment::Center, Direction::Horizontal) => {
          local_position.x += (ctx.measure.size.x - cur_line.content_size.x) / 2. - self.padding.left;
        },
        (Alignment::Center, Direction::Vertical) => {
          local_position.y += (ctx.measure.size.y - cur_line.content_size.y) / 2. - self.padding.top;
        },
        (Alignment::End, Direction::Horizontal) => {
          local_position.x += ctx.measure.size.x - cur_line.content_size.x - self.padding.right - self.padding.left;
        },
        (Alignment::End, Direction::Vertical) => {
          local_position.y += ctx.measure.size.y - cur_line.content_size.y - self.padding.bottom - self.padding.top;
        }
      }

      let next_line_begin = user_data.lines
        .get(line_idx + 1)
        .map(|l| l.start_idx)
        .unwrap_or(self.children.0.len());

      for element_idx in cur_line.start_idx..next_line_begin {
        let element = &self.children.0[element_idx];

        //(passing max size from layout rather than actual known bounds for the sake of consistency with measure() above)
        //... as this must match!

        let mut el_layout = LayoutInfo {
          position: local_position,
          max_size: self.measure_max_inner_size(ctx.layout),
          direction: self.direction,
        };

        //measure
        let el_measure = element.measure(MeasureContext {
          state: ctx.state,
          layout: &el_layout,
          text_measure: ctx.text_measure,
          current_font: ctx.current_font,
          images: ctx.images,
        });

        //align (on sec. axis)
        //TODO separate align withing the line and align of the whole line
        let inner_content_size = ctx.measure.hints.inner_content_size.unwrap();
        match (pri_sec_align.1, self.direction) {
          (Alignment::Begin, _) => (),
          (Alignment::Center, Direction::Horizontal) => {
            //Align whole row
            el_layout.position.y += ((ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y) / 2.;
            //Align within row
            el_layout.position.y += (cur_line.content_size.y - el_measure.size.y) / 2.;
          },
          (Alignment::Center, Direction::Vertical) => {
            //Align whole row
            el_layout.position.x += ((ctx.measure.size.x - self.padding.left - self.padding.right) - inner_content_size.x) / 2.;
            //Align within row
            el_layout.position.x += (cur_line.content_size.x - el_measure.size.x) / 2.;
          },
          //TODO update these two cases:
          (Alignment::End, Direction::Horizontal) => {
            //Align whole row
            el_layout.position.y += (ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y;
            //Align within row
            el_layout.position.y += cur_line.content_size.y - el_measure.size.y;
          },
          (Alignment::End, Direction::Vertical) => {
            //Align whole row
            el_layout.position.x += (ctx.measure.size.x - self.padding.right - self.padding.left) - inner_content_size.x;
            //Align within row
            el_layout.position.x += cur_line.content_size.x - el_measure.size.x;
          }
        }

        //process
        element.process(ProcessContext {
          measure: &el_measure,
          state: ctx.state,
          layout: &el_layout,
          draw: ctx.draw,
          text_measure: ctx.text_measure,
          current_font: ctx.current_font,
          images: ctx.images,
          input: ctx.input,
          //HACK: i have no idea what to do with this
          //this sucks
          signal: ctx.signal,
        });

        //layout
        match self.direction {
          Direction::Horizontal => {
            local_position.x += el_measure.size.x + self.gap;
          },
          Direction::Vertical => {
            local_position.y += el_measure.size.y + self.gap;
          }
        }
      }

      //Move to the next line
      match self.direction {
        Direction::Horizontal => {
          position.y += cur_line.content_size.y + self.gap;
          //position.x -= cur_line.content_size.x;
          // leftover_line_gap = vec2(0., self.gap);
        }
        Direction::Vertical => {
          position.x += cur_line.content_size.x + self.gap;
          //position.y -= cur_line.content_size.y;
          // leftover_line_gap = vec2(self.gap, 0.);
        }
      };
    }
  }
}