package com.huantansheng.easyphotos.models.puzzle.straight;

import android.graphics.PointF;
import android.graphics.RectF;
import android.util.Pair;

import com.huantansheng.easyphotos.models.puzzle.Area;
import com.huantansheng.easyphotos.models.puzzle.Line;
import com.huantansheng.easyphotos.models.puzzle.PuzzleLayout;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import static com.huantansheng.easyphotos.models.puzzle.straight.StraightUtils.createLine;
import static com.huantansheng.easyphotos.models.puzzle.straight.StraightUtils.cutAreaCross;
import static com.huantansheng.easyphotos.models.puzzle.straight.StraightUtils.cutAreaSpiral;


/**
 * @author wupanjie
 */
public abstract class StraightPuzzleLayout implements PuzzleLayout {
  private RectF bounds;
  private StraightArea outerArea;

  private List<StraightArea> areas = new ArrayList<>();
  private List<Line> lines = new ArrayList<>();
  private List<Line> outerLines = new ArrayList<>(4);

  private float padding;
  private float radian;
  private int color;

  private Comparator<StraightArea> areaComparator = new StraightArea.AreaComparator();

  private ArrayList<Step> steps = new ArrayList<>();

  protected StraightPuzzleLayout() {

  }

  @Override
  public void setOuterBounds(RectF bounds) {
    reset();

    this.bounds = bounds;

    PointF one = new PointF(bounds.left, bounds.top);
    PointF two = new PointF(bounds.right, bounds.top);
    PointF three = new PointF(bounds.left, bounds.bottom);
    PointF four = new PointF(bounds.right, bounds.bottom);

    StraightLine lineLeft = new StraightLine(one, three);
    StraightLine lineTop = new StraightLine(one, two);
    StraightLine lineRight = new StraightLine(two, four);
    StraightLine lineBottom = new StraightLine(three, four);

    outerLines.clear();

    outerLines.add(lineLeft);
    outerLines.add(lineTop);
    outerLines.add(lineRight);
    outerLines.add(lineBottom);

    outerArea = new StraightArea();
    outerArea.lineLeft = lineLeft;
    outerArea.lineTop = lineTop;
    outerArea.lineRight = lineRight;
    outerArea.lineBottom = lineBottom;

    areas.clear();
    areas.add(outerArea);
  }

  @Override
  public abstract void layout();

  @Override
  public int getAreaCount() {
    return areas.size();
  }

  @Override
  public List<Line> getOuterLines() {
    return outerLines;
  }

  @Override
  public List<Line> getLines() {
    return lines;
  }

  @Override
  public void update() {
    for (Line line : lines) {
      line.update(width(), height());
    }
  }

  @Override
  public float width() {
    return outerArea == null ? 0 : outerArea.width();
  }

  @Override
  public float height() {
    return outerArea == null ? 0 : outerArea.height();
  }

  @Override
  public void reset() {
    lines.clear();
    areas.clear();
    areas.add(outerArea);
    steps.clear();
  }

  @Override
  public Area getArea(int position) {
    return areas.get(position);
  }

  @Override

  public StraightArea getOuterArea() {
    return outerArea;
  }

  @Override
  public void setPadding(float padding) {
    this.padding = padding;

    for (Area area : areas) {
      area.setPadding(padding);
    }

    outerArea.lineLeft.startPoint().set(bounds.left + padding, bounds.top + padding);
    outerArea.lineLeft.endPoint().set(bounds.left + padding, bounds.bottom - padding);

    outerArea.lineRight.startPoint().set(bounds.right - padding, bounds.top + padding);
    outerArea.lineRight.endPoint().set(bounds.right - padding, bounds.bottom - padding);

    update();
  }

  @Override
  public float getPadding() {
    return padding;
  }

  protected void addLine(int position, Line.Direction direction, float ratio) {
    StraightArea area = areas.get(position);
    addLine(area, direction, ratio);

    Step step = new Step();
    step.type = Step.ADD_LINE;
    step.direction = direction == Line.Direction.HORIZONTAL ? 0 : 1;
    step.position = position;
    steps.add(step);
  }

  private List<StraightArea> addLine(StraightArea area, Line.Direction direction, float ratio) {
    areas.remove(area);
    StraightLine line = createLine(area, direction, ratio);
    lines.add(line);

    List<StraightArea> increasedArea = StraightUtils.cutArea(area, line);
    areas.addAll(increasedArea);

    updateLineLimit();
    sortAreas();

    return increasedArea;
  }

  protected void cutAreaEqualPart(int position, int part, Line.Direction direction) {
    StraightArea temp = areas.get(position);
    for (int i = part; i > 1; i--) {
      temp = addLine(temp, direction, (float) (i - 1) / i).get(0);
    }

    Step step = new Step();
    step.type = Step.CUT_EQUAL_PART_TWO;
    step.part = part;
    step.position = position;
    step.direction = direction == Line.Direction.HORIZONTAL ? 0 : 1;
    steps.add(step);
  }

  protected void addCross(int position, float ratio) {
    addCross(position, ratio, ratio);
  }

  protected void addCross(int position, float horizontalRatio, float verticalRatio) {
    StraightArea area = areas.get(position);
    areas.remove(area);
    StraightLine horizontal = createLine(area, Line.Direction.HORIZONTAL, horizontalRatio);
    StraightLine vertical = createLine(area, Line.Direction.VERTICAL, verticalRatio);
    lines.add(horizontal);
    lines.add(vertical);

    List<StraightArea> newAreas = cutAreaCross(area, horizontal, vertical);
    areas.addAll(newAreas);

    updateLineLimit();
    sortAreas();

    Step step = new Step();
    step.type = Step.ADD_CROSS;
    step.position = position;
    steps.add(step);
  }

  protected void cutAreaEqualPart(int position, int hSize, int vSize) {
    StraightArea area = areas.get(position);
    areas.remove(area);
    Pair<List<StraightLine>, List<StraightArea>> increased =
        StraightUtils.cutArea(area, hSize, vSize);
    List<StraightLine> newLines = increased.first;
    List<StraightArea> newAreas = increased.second;

    lines.addAll(newLines);
    areas.addAll(newAreas);

    updateLineLimit();
    sortAreas();

    Step step = new Step();
    step.type = Step.CUT_EQUAL_PART_ONE;
    step.position = position;
    step.hSize = hSize;
    step.vSize = vSize;
    steps.add(step);
  }

  protected void cutSpiral(int position) {
    StraightArea area = areas.get(position);
    areas.remove(area);
    Pair<List<StraightLine>, List<StraightArea>> spilt = cutAreaSpiral(area);

    lines.addAll(spilt.first);
    areas.addAll(spilt.second);

    updateLineLimit();
    sortAreas();

    Step step = new Step();
    step.type = Step.CUT_SPIRAL;
    step.position = position;
    steps.add(step);
  }

  private void sortAreas() {
    Collections.sort(areas, areaComparator);
  }

  private void updateLineLimit() {
    int size = lines.size();
    for (int i = 0; i < size; i++) {
      Line line = lines.get(i);
      updateUpperLine(line);
      updateLowerLine(line);
    }
  }

  private void updateLowerLine(final Line line) {
    int size = lines.size();
    for (int i = 0; i < size; i++) {
      Line l = lines.get(i);
      if (l == line) {
        continue;
      }

      if (l.direction() != line.direction()) {
        continue;
      }

      if (l.direction() == Line.Direction.HORIZONTAL) {
        if (l.maxX() <= line.minX() || line.maxX() <= l.minX()) continue;
        if (l.minY() > line.lowerLine().maxY() && l.maxY() < line.minY()) {
          line.setLowerLine(l);
        }
      } else {
        if (l.maxY() <= line.minY() || line.maxY() <= l.minY()) continue;
        if (l.minX() > line.lowerLine().maxX() && l.maxX() < line.minX()) {
          line.setLowerLine(l);
        }
      }
    }
  }

  private void updateUpperLine(final Line line) {
    int size = lines.size();
    for (int i = 0; i < size; i++) {
      Line l = lines.get(i);
      if (l == line) {
        continue;
      }

      if (l.direction() != line.direction()) {
        continue;
      }

      if (l.direction() == Line.Direction.HORIZONTAL) {
        if (l.maxX() <= line.minX() || line.maxX() <= l.minX()) continue;
        if (l.maxY() < line.upperLine().minY() && l.minY() > line.maxY()) {
          line.setUpperLine(l);
        }
      } else {
        if (l.maxY() <= line.minY() || line.maxY() <= l.minY()) continue;
        if (l.maxX() < line.upperLine().minX() && l.minX() > line.maxX()) {
          line.setUpperLine(l);
        }
      }
    }
  }

  @Override
  public float getRadian() {
    return radian;
  }

  @Override
  public void setRadian(float radian) {
    this.radian = radian;
    for (Area area : areas) {
      area.setRadian(radian);
    }
  }

  @Override
  public int getColor() {
    return color;
  }

  @Override
  public void setColor(int color) {
    this.color = color;
  }

  @Override
  public Info generateInfo() {
    Info info = new Info();
    info.type = Info.TYPE_STRAIGHT;
    info.padding = padding;
    info.radian = radian;
    info.color = color;
    info.steps = steps;
    ArrayList<LineInfo> lineInfos = new ArrayList<>();
    for (Line line : lines) {
      LineInfo lineInfo = new LineInfo(line);
      lineInfos.add(lineInfo);
    }
    info.lineInfos = lineInfos;
    return info;
  }
}
