package collide.demo.view;

import collide.client.util.CssUtils;
import collide.demo.resources.DemoResources;
import collide.plugin.client.inspector.ElementInspector;
import com.google.collide.json.client.JsoArray;
import elemental.client.Browser;
import elemental.dom.Document;
import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventRemover;
import elemental.events.MouseEvent;
import elemental.util.Timer;
import xapi.log.X_Log;
import xapi.util.api.RemovalHandler;

import com.google.gwt.resources.client.CssResource;

import java.util.Comparator;
import java.util.TreeMap;

public class SplitPanel {

  public static interface Css extends CssResource{
    String splitPanel();
    String splitPanelChild();
    String panelContent();
    String verticalSplit();
    String head();
    String horizontalSplit();
    String bottomSplit();
    String leftSplit();
    String rightSplit();
    String tail();
    String topSplit();
  }

  class PanelNode {
    Element el;
    PanelNode next;
    double size;
  }

  final PanelNode head;
  PanelNode tail;
  int size;
  private boolean vertical;
  private Timer refresh;
  private Css css;

  public SplitPanel(boolean vertical) {
    this.vertical = vertical;
    css = createCss();
    tail = head = new PanelNode();
    head.el = Browser.getDocument().createDivElement();
    head.el.addClassName(css.splitPanel());
    head.el.addClassName(vertical ? css.verticalSplit() : css.horizontalSplit());
  }

  public Element getElement() {
    return head.el;
  }

  public RemovalHandler addChild(Element child, double width) {
    return addChild(child, width, -1);
  }

  public RemovalHandler addChild(final Element child, double width, int index) {
    if (exists(child)) {
      // TODO adjust index
      return RemovalHandler.DoNothing;
    }
    final PanelNode node = new PanelNode();
    node.size = width;
    node.el = wrapChild(child, node);
    getElement().appendChild(node.el);
    RemovalHandler remover = new RemovalHandler() {
      @Override
      public void remove() {
        SplitPanel.this.remove(node);
      }

    };
    size ++;
    if (index < 0) {
      // Negative index = add-to-end, the simplest and fastest case.
      tail.next = node;
      tail.el.removeClassName(css.tail());
      node.el.addClassName(css.tail());
      if (tail == head) {
        node.el.addClassName(css.head());
      }
      tail = node;
    } else {
      PanelNode target = head;
      while (index --> 0) {
        if (target.next==null)
          break;
        target = target.next;
      }
      if (target.next == null) {
        // We hit the end, add to end and update tail
        target.next = node;
        tail.el.removeClassName(css.tail());
        node.el.addClassName(css.tail());
        if (tail == head) {
          node.el.addClassName(css.head());
        }
        tail = node;
      } else {
        // An actual insert, just update pointers
        if (target == head) {
          if (head.next != null) {
            head.next.el.removeClassName(css.head());
          }
          node.el.addClassName(css.head());
        }
        node.next = target.next;
        target.next = node;
      }
    }
    if (refresh == null) {
      refresh = new Timer() {
        @Override
        public void run() {
          refresh = null;
          refresh();
        }
      };
      refresh.schedule(1);
    }
    return remover;
  }

  public void remove(Element el) {
    PanelNode node = head;
    while (node != null) {
      if (node.el == el) {
        remove(node);
        return;
      }
      node = node.next;
    }
  }
  private void remove(PanelNode node) {
    if (head.next == node) {
      node.el.removeClassName(css.head());
      node.el.removeFromParent();
      if (node.next != null) {
        node.next.el.addClassName(css.head());
      }
      head.next = node.next;
      if (isElastic(node)) {
        giveSpace(node.size);
      }
      size--;
      refresh();
      return;
    }
    PanelNode target = head;
    while (target.next != null) {
      if (target.next == node) {
        if (node == tail) {
          node.el.removeClassName(css.tail());
          target.el.addClassName(css.tail());
          tail = target;
        }
        node.el.removeFromParent();
        target.next = node.next;
        if (isElastic(node)) {
          giveSpace(node.size);
        }
        size--;
        refresh();
        return;
      }
      target = target.next;
    }
  }

  private void giveSpace(double size) {
    PanelNode search = head.next;
    while (search != null) {
      if (isElastic(search)) {
        search.size += size;
        return;
      }
      search = search.next;
    }

  }

  private boolean exists(Element child) {
    PanelNode search = head.next;
    while (search != null) {
      if (search.el == child)
        return true;
      search = search.next;
    }
    return false;
  }

  protected int getMaxDimension() {
    return vertical ? head.el.getClientHeight() : head.el.getClientWidth();
  }

  public void refresh() {
    if (head.next == null)
      return;
    // Layout panels.
    PanelNode node = head;
    double percents = 0;
    double px = 0;
    float max = getMaxDimension();
    JsoArray<PanelNode> fills = JsoArray.create();
    JsoArray<PanelNode> fixed = JsoArray.create();
    while (node.next != null) {
      node = node.next;
      if (node.size < 0)
        fills.add(node);
      else {
        fixed.add(node);
        if (node.size < 1){
          assert node.size != 0;
          percents += node.size;
        }
        else
          px += node.size;
      }
    }
    final int numFills = fills.size();
    final int numFixed = fixed.size();
    double x = 0;
    boolean overflow = false;
    if (numFixed == 0) {
      // No fixed size items, just lay everything out evenly.
      double w = max / numFills;
      for (int i = 0; i < numFills; i++)
        x = setSize(fills.get(i), w, x);
    } else {
      // Some fixed / percent size items.
      double size = px + percents * max;
      double scale = size / max;
      if (numFills == 0) {
        // No fills, distribute proportionally
        for (int i = 0;i < numFixed; i++) {
          PanelNode child = fixed.get(i);
          x = setSize(child, (child.size < 1 ? size * child.size : child.size) / scale, x);
        }
      } else {
        // Some fixed, some fills; things could get messy...
        double fillSize;
        overflow = scale > 1;
        if (overflow) {
            // We have an effin' mess! Just scroll everything off the end
          fillSize = max * .25; // Fills get 25% screen size
          scale = 1; // Everything else gets what it asked for
          size = max;
        } else {
          // There's enough to give everyone what they want, and give the rest to fills.
          fillSize = Math.max(350, (max - size) / numFills);

        }
        node = head; // We have to iterate all nodes
        while (node.next != null) {
          node = node.next;
          if (node.size < 0) {
            // A fill. Give it our leftover size
            x = setSize(node, fillSize, x);
          } else if (node.size < 1) {
            // A percent.  Give it a ratio of max
            x = setSize(node, node.size * size
//                size / scale
                , x);
          } else {
            // A fixed size. Scale it up / down.
            x = setSize(node, node.size, x);
          }
        }
      }
      enableOverflow(overflow);
    }
  }

  protected Element wrapChild(Element child, PanelNode node) {
    ElementInspector.get().makeInspectable(child, node);
    // Throw in some drag handles.
    Document doc = Browser.getDocument();
    Element wrapper = doc.createDivElement();
    child.addClassName(css.panelContent());
    wrapper.addClassName(css.splitPanelChild());
    wrapper.getStyle().setPosition("absolute");
    wrapper.appendChild(child);
    createSlider(wrapper, child, node, true);
    createSlider(wrapper, child, node, false);
    return wrapper;
  }

  private Css createCss() {
    Css css = DemoResources.GLOBAL.splitPanel();
    css.ensureInjected();
    return css;
  }

  static class PanelPosition {
    float posStart;
    float sizeStart;
    float mouseStart;
    PanelNode node;
    PanelPosition next;
  }

  private void createSlider(final Element wrapper, final Element child,
      final PanelNode node, final boolean first) {
    final Element sliderEl = Browser.getDocument().createSpanElement();
    final String cls;
    if (vertical) {
      if (first) {
        // Do a top-slider
        cls = css.topSplit();
      } else {
        // Do a bottom-slider
        cls = css.bottomSplit();
      }
    } else {
      if (first) {
        // Do a left-slider
        cls = css.leftSplit();
      } else {
        // Do a right-slider
        cls = css.rightSplit();
      }
    }
    sliderEl.addClassName(cls);
    wrapper.appendChild(sliderEl);
    final PanelPosition self = new PanelPosition();
    self.node = node;

    sliderEl.setOnmousedown(new EventListener() {
      @Override
      public void handleEvent(Event evt) {
        evt.preventDefault();
        final PanelNode siblingNode = first ? getPreviousSibling(node) : node.next;
        final Element sibling;
        if (siblingNode == null) {
          sibling = null;
        } else {
          self.next = new PanelPosition();
          sibling = siblingNode.el;
          self.next.node = siblingNode;
        }
        PanelPosition affected = self.next;
        MouseEvent e = (MouseEvent) evt;
        if (vertical) {
          self.mouseStart = e.getClientY();
          // We parse from the css values set (which we set before this is called),
          // so we don't have to do any measuring
          // or compensate for weird offsets in the client elements.
          self.posStart = CssUtils.parsePixels(wrapper.getStyle().getTop());
          self.sizeStart = CssUtils.parsePixels(wrapper.getStyle().getHeight());
          if (sibling != null) {
            affected.posStart = CssUtils.parsePixels(sibling.getStyle().getTop());
            affected.sizeStart = CssUtils.parsePixels(sibling.getStyle().getHeight());
          }
        } else {
          self.mouseStart = e.getClientX();
          self.posStart = CssUtils.parsePixels(wrapper.getStyle().getLeft());
          self.sizeStart = CssUtils.parsePixels(wrapper.getStyle().getWidth());
          if (sibling != null) {
            affected.posStart = CssUtils.parsePixels(sibling.getStyle().getLeft());
            affected.sizeStart = CssUtils.parsePixels(sibling.getStyle().getWidth());
          }
        }

        final EventRemover[] remover = new EventRemover[2];
        // TODO put these event listeners over an empty iframe, to cover up any iframes on page.
        remover[0] = Browser.getWindow().addEventListener("mouseup", new EventListener() {
          @Override
          public void handleEvent(Event evt) {
            if (remover[0] != null){
              remover[0].remove();
              remover[1].remove();
              onResizeFinished(siblingNode, self);
              remover[0] = null;
            }
          }
        }, true);
        remover[1] = Browser.getWindow().addEventListener("mousemove", new EventListener() {
          @Override
          public void handleEvent(Event evt) {
            evt.preventDefault();
            final MouseEvent e = (MouseEvent) evt;
            final Element el = self.next == null ? null : self.next.node.el;
            assert el != wrapper;
            double delta;
            if (vertical) {
              delta = e.getClientY() - self.mouseStart;
              if (first) {
                // A top drag affects top and height
                wrapper.getStyle().setTop((int)(self.posStart+ delta),"px");
                wrapper.getStyle().setHeight((int)(self.sizeStart - delta),"px");
                if (el != null) {
                  // TODO implement max/min/pref values, and iterate through the
                  // nodes to push delta off on any neighbors
                  el.getStyle().setHeight((int)(self.next.sizeStart + delta),"px");
                }
              } else {
                wrapper.getStyle().setHeight((int)(self.sizeStart + delta),"px");
                if (el != null) {
                  el.getStyle().setTop((int)(self.next.posStart+ delta),"px");
                  el.getStyle().setHeight((int)(self.next.sizeStart - delta),"px");
                }
              }
            } else {
              delta = e.getClientX() - self.mouseStart;
              if (first) {
                // A left drag affects left and size
                wrapper.getStyle().setLeft((int)(self.posStart + delta),"px");
                wrapper.getStyle().setWidth((int)(self.sizeStart - delta),"px");
                if (el != null) {
                  el.getStyle().setWidth((int)(self.next.sizeStart + delta),"px");
                }
              } else {
                wrapper.getStyle().setWidth((int)(self.sizeStart + delta),"px");
                if (el != null) {
                  el.getStyle().setLeft((int)(self.next.posStart + delta),"px");
                  el.getStyle().setWidth((int)(self.next.sizeStart - delta),"px");
                }
              }
            }
          }
        }, true);


      }
    });
  }

  private void onResizeFinished(PanelNode siblingNode, PanelPosition self) {
    if (self.node.size < 1) {
      if (self.node.size > 0) {
        // A fractional panel should get it's fraction updated
        double size = getSize(self.node),
        max = getMaxDimension(),
        current = size / max,
        is = self.node.size,
        delta = is - current;

        X_Log.info("Resize done; was ",self.node.size, " is ",current);
        self.node.size = current;
        stealSize(self.node, delta);
      }
    }
  }

  private static final Comparator<Double> cmp = new Comparator<Double>() {
    @Override
    public int compare(Double o1, Double o2) {
      return o1 < o2 ? 1 : -1; // largest to smallest
    }
  };

  private void stealSize(PanelNode node, double delta) {
    PanelNode search = head;
    TreeMap<Double, PanelNode> areElastic = new TreeMap<Double, PanelNode>(cmp);
    do {
      if (isElastic(search)) {
        if (search != node) {
          areElastic.put(search.size, search);
        }
      }
      search = search.next;
    } while (search != null);
    int size = areElastic.size();
    if (size == 0) {
      return;
    }
    X_Log.info(getClass(), "Stealing size ", delta, " for ", node,"from",areElastic.values().toArray());
    if (size == 1) {
      PanelNode from = areElastic.values().iterator().next();
      double was = from.size
          ,is = 1 - node.size,
          round = is - Math.floor(10000.0 * is)/10000.0;
      from.size = is > from.size ? is - round : is + round;
      X_Log.info("Size changed from ", was, "to", from.size, "for",from);
      return;
    }
    double[] avail = new double[size];
    int pos = 0;
    for (PanelNode from : areElastic.values()) {
      if (from.size <= 0.1) {
        continue;
      }
      double has = from.size + delta;
      if (has > 1) {
        has = has - 1;
//        has = has - 1;
//        if (has)
      } else if (0 > has) {
        avail[pos] = 0;
      } else {
        avail[pos] = from.size - has;
      }
      pos ++;
    }

  }

  protected boolean isElastic(PanelNode search) {
    return search.size > 0 && search.size < 1;
  }

  private double getSize(PanelNode node) {
    return vertical
        ? node.el.getClientHeight()
        : node.el.getClientWidth();
  }

  private PanelNode getPreviousSibling(PanelNode node) {
    if (node == head.next)
      return null;
    PanelNode search = head;
    while (search.next != null) {
      if (search.next == node) {
        return search;
      }
      search = search.next;
    }
    return null;
  }

  private void enableOverflow(boolean enable) {
    if (enable) {
      if (vertical) {
        head.el.getStyle().setOverflowY("auto");
      } else {
        head.el.getStyle().setOverflowX("auto");
      }
    } else {
      if (vertical) {
        head.el.getStyle().clearOverflowY();
      } else {
        head.el.getStyle().clearOverflowX();
      }
    }
  }

  private double setSize(PanelNode panelNode, double w, double x) {
    if (vertical) {
      panelNode.el.getStyle().setHeight((int)w, "px");
      panelNode.el.getStyle().setTop((int)x, "px");
    } else {
      panelNode.el.getStyle().setWidth((int)w, "px");
      panelNode.el.getStyle().setLeft((int)x, "px");
    }
    return x+w;
  }

}
