/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.myfaces.tobago.internal.component;

import org.apache.commons.lang.StringUtils;
import org.apache.myfaces.tobago.component.SupportsMarkup;
import org.apache.myfaces.tobago.context.ClientProperties;
import org.apache.myfaces.tobago.internal.layout.BankHead;
import org.apache.myfaces.tobago.internal.layout.Cell;
import org.apache.myfaces.tobago.internal.layout.FactorList;
import org.apache.myfaces.tobago.internal.layout.Grid;
import org.apache.myfaces.tobago.internal.layout.Interval;
import org.apache.myfaces.tobago.internal.layout.IntervalList;
import org.apache.myfaces.tobago.internal.layout.LayoutUtils;
import org.apache.myfaces.tobago.internal.layout.OriginCell;
import org.apache.myfaces.tobago.layout.AutoLayoutToken;
import org.apache.myfaces.tobago.layout.Display;
import org.apache.myfaces.tobago.layout.LayoutComponent;
import org.apache.myfaces.tobago.layout.LayoutContainer;
import org.apache.myfaces.tobago.layout.LayoutManager;
import org.apache.myfaces.tobago.layout.LayoutToken;
import org.apache.myfaces.tobago.layout.LayoutTokens;
import org.apache.myfaces.tobago.layout.Measure;
import org.apache.myfaces.tobago.layout.Orientation;
import org.apache.myfaces.tobago.layout.PixelLayoutToken;
import org.apache.myfaces.tobago.layout.RelativeLayoutToken;
import org.apache.myfaces.tobago.util.VariableResolverUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.faces.context.FacesContext;
import java.util.List;

public abstract class AbstractUIGridLayout extends AbstractUILayoutBase implements LayoutManager, SupportsMarkup {

  private static final Logger LOG = LoggerFactory.getLogger(AbstractUIGridLayout.class);

  private Grid grid;

  /**
   * Initialize the grid and remove the current width and height values from the component, recursively.
   */
  public void init() {

    if (!getLayoutContainer().isLayoutChildren()) {
      return;
    }

    grid = new Grid(LayoutTokens.parse(getColumns()), LayoutTokens.parse(getRows()));

    List<LayoutComponent> components = getLayoutContainer().getComponents();
    for (LayoutComponent component : components) {
      component.setCurrentHeight(null);
      component.setCurrentWidth(null);
      grid.add(new OriginCell(component), component.getColumnSpan(), component.getRowSpan());
      if (LOG.isDebugEnabled()) {
        LOG.debug("\n" + grid);
      }
      if (component instanceof LayoutContainer && (component.isRendered() || isRigid())) {
        ((LayoutContainer) component).getLayoutManager().init();
      }
    }

    grid.setColumnOverflow(isColumnOverflow());
    grid.setRowOverflow(isRowOverflow());
  }

  public void fixRelativeInsideAuto(Orientation orientation, boolean auto) {

    if (!getLayoutContainer().isLayoutChildren()) {
      return;
    }

    BankHead[] heads = grid.getBankHeads(orientation);
    BankHead[] heads2 = grid.getBankHeads(orientation.other());

    if (auto) {
      for (int i = 0; i < heads.length; i++) {
        if (heads[i].getToken() instanceof RelativeLayoutToken) {
          if (LOG.isDebugEnabled()) {
            LOG.debug("Fixing layout token from * to auto, because a * in not allowed inside of a auto. "
                + "Token * at index=" + i + ", orientation=" + orientation + ", grid=\n" + grid);
          }
          heads[i].setToken(AutoLayoutToken.INSTANCE);
        }
      }
    }

    for (int i = 0; i < heads.length; i++) {
      boolean neitherRendered = true;
      for (int j = 0; j < heads2.length; j++) {
        Cell cell = grid.getCell(i, j, orientation);
        // check rendered = false
        if (cell != null && cell.getComponent().isRendered()) {
          neitherRendered = false;
        }
        // recursion
        if (cell instanceof OriginCell) {
          OriginCell origin = (OriginCell) cell;
          LayoutComponent component = cell.getComponent();
          if (component instanceof LayoutContainer && component.isRendered()) {
            LayoutManager layoutManager = ((LayoutContainer) component).getLayoutManager();
            // TODO: may be improved
            boolean childAuto = origin.getSpan(orientation) == 1 && heads[i].getToken() instanceof AutoLayoutToken;
            layoutManager.fixRelativeInsideAuto(orientation, childAuto);
          }
        }
      }
      if (neitherRendered && !isRigid()) {
        heads[i].setRendered(false);
      }
    }
  }

  public void preProcessing(Orientation orientation) {

    if (!getLayoutContainer().isLayoutChildren()) {
      return;
    }

    final BankHead[] heads = grid.getBankHeads(orientation);
    final BankHead[] heads2 = grid.getBankHeads(orientation.other());

    // process auto tokens
    int i = 0;

    for (BankHead head : heads) {
      LayoutToken token = head.getToken();

      if (token instanceof PixelLayoutToken) {
        if (head.isRendered() || isRigid()) {
          int pixel = ((PixelLayoutToken) token).getPixel();
          heads[i].setCurrent(Measure.valueOf(pixel)); // XXX refactor
        } else {
          heads[i].setCurrent(Measure.ZERO);
        }
      }

      IntervalList intervalList = new IntervalList();
      for (int j = 0; j < heads2.length; j++) {
        Cell cell = grid.getCell(i, j, orientation);
        if (cell instanceof OriginCell) {
          OriginCell origin = (OriginCell) cell;
          LayoutComponent component = cell.getComponent();

          if (component instanceof LayoutContainer && (component.isRendered() || isRigid())) {
            ((LayoutContainer) component).getLayoutManager().preProcessing(orientation);
          }

          if (token instanceof AutoLayoutToken || token instanceof RelativeLayoutToken) {
            if (origin.getSpan(orientation) == 1 && (component.isRendered() || isRigid())) {
              intervalList.add(new Interval(component, orientation));
            } else {
              if (LOG.isDebugEnabled()) {
                LOG.debug("Components with span > 1 will be ignored in 'auto' layout rows/columns.");
                // todo: give this information to the developer
              }
            }
          }
        }
      }

      intervalList.evaluate();
      if (token instanceof AutoLayoutToken || token instanceof RelativeLayoutToken) {
        heads[i].setIntervalList(intervalList);
      }
      if (token instanceof AutoLayoutToken) {
        heads[i].setCurrent(intervalList.getCurrent());
      }
      i++;
    }

/*
    IntervalList relatives = new IntervalList();
    for (BankHead head : heads) {
      LayoutToken token = head.getToken();
      if (token instanceof RelativeLayoutToken) {
        final int factor = ((RelativeLayoutToken) token).getFactor();
        for (Interval interval : head.getIntervalList()) {
          relatives.add(new Interval(interval, factor));
        }
      }
    }
    relatives.evaluate();
*/

    // set the size if all sizes of the grid are set
    Measure sum = Measure.ZERO;
    for (BankHead head : heads) {
      Measure size = null;
      final LayoutToken token = head.getToken();
      if (token instanceof RelativeLayoutToken) {
//        final int factor = ((RelativeLayoutToken) token).getFactor();
//        size = relatives.getCurrent().multiply(factor);
      } else {
        size = head.getCurrent();
      }
      if (size == null) {
        sum = null; // set to invalid
        break;
//        LOG.error("TODO: Should not happen!");
      }
      sum = sum.add(size);
    }
    if (sum != null) {
      // adding the space between the cells
      sum = sum.add(LayoutUtils.getBorderBegin(orientation, getLayoutContainer()));
      sum = sum.add(LayoutUtils.getPaddingBegin(orientation, getLayoutContainer()));
      sum = sum.add(getMarginBegin(orientation));
      sum = sum.add(computeSpacing(orientation, 0, heads.length));
      sum = sum.add(getMarginEnd(orientation));
      sum = sum.add(LayoutUtils.getPaddingEnd(orientation, getLayoutContainer()));
      sum = sum.add(LayoutUtils.getBorderEnd(orientation, getLayoutContainer()));
      LayoutUtils.setCurrentSize(orientation, getLayoutContainer(), sum);
    }
  }

  public void mainProcessing(Orientation orientation) {

    if (!getLayoutContainer().isLayoutChildren()) {
      return;
    }

    final BankHead[] heads = grid.getBankHeads(orientation);
    final BankHead[] heads2 = grid.getBankHeads(orientation.other());

    // find *
    FactorList factorList = new FactorList();
    for (BankHead head : heads) {
      if (head.getToken() instanceof RelativeLayoutToken && head.isRendered()) {
        factorList.add(((RelativeLayoutToken) head.getToken()).getFactor());
      }
    }
    if (!factorList.isEmpty()) {
      // find rest
      LayoutContainer container = getLayoutContainer();
      Measure available = LayoutUtils.getCurrentSize(orientation, container);
      if (available != null) {
        for (BankHead head : heads) {
          available = available.subtractNotNegative(head.getCurrent());
        }
        available = available.subtractNotNegative(LayoutUtils.getBorderBegin(orientation, container));
        available = available.subtractNotNegative(LayoutUtils.getPaddingBegin(orientation, container));
        available = available.subtractNotNegative(getMarginBegin(orientation));
        available = available.subtractNotNegative(computeSpacing(orientation, 0, heads.length));
        available = available.subtractNotNegative(getMarginEnd(orientation));
        available = available.subtractNotNegative(LayoutUtils.getPaddingEnd(orientation, container));
        available = available.subtractNotNegative(LayoutUtils.getBorderEnd(orientation, container));

        if (grid.isOverflow(orientation.other())) {
          ClientProperties client = VariableResolverUtils.resolveClientProperties(FacesContext.getCurrentInstance());
          final Measure scrollbar = orientation
              == Orientation.HORIZONTAL ? client.getVerticalScrollbarWeight() : client.getHorizontalScrollbarWeight();
          available = available.subtractNotNegative(scrollbar);
        }

        List<Measure> partition = factorList.partition(available);

        // write values back into the header
        int i = 0; // index of head
        int j = 0; // index of partition
        for (BankHead head : heads) {
          if (head.getToken() instanceof RelativeLayoutToken && head.isRendered()) {
            // respect the minimum
            heads[i].setCurrent(Measure.max(partition.get(j), heads[i].getIntervalList().getMinimum()));
            j++;
          }
          i++;
        }
      } else {
        LOG.warn("No width/height set but needed for *!"); // todo: more information
      }
    }

    // call manage sizes for all sub-layout-managers
    for (int i = 0; i < heads.length; i++) {
      for (int j = 0; j < heads2.length; j++) {
        Cell cell = grid.getCell(i, j, orientation);
        if (cell instanceof OriginCell) {
          LayoutComponent component = cell.getComponent();

          component.setDisplay(Display.BLOCK); // TODO: use CSS via classes and style.css

          Integer span = ((OriginCell) cell).getSpan(orientation);

          // compute the size of the cell
          Measure size = Measure.ZERO;
          for (int k = 0; k < span; k++) {
            size = size.add(heads[i + k].getCurrent());
          }
          size = size.add(computeSpacing(orientation, i, span));
          Measure current = LayoutUtils.getCurrentSize(orientation, component);
          if (current == null) {
            LayoutUtils.setCurrentSize(orientation, component, size);
          }

          // call sub layout manager
          if (component instanceof LayoutContainer && (component.isRendered() || isRigid())) {
            ((LayoutContainer) component).getLayoutManager().mainProcessing(orientation);
          }
        }
      }
    }

    Measure size = Measure.ZERO;
    size = size.add(LayoutUtils.getPaddingBegin(orientation, getLayoutContainer()));
    size = size.add(getMarginBegin(orientation));
    size = size.add(computeSpacing(orientation, 0, heads.length));
    for (BankHead head : heads) {
      size = size.add(head.getCurrent());
    }
    size = size.add(getMarginEnd(orientation));
    size = size.add(LayoutUtils.getPaddingEnd(orientation, getLayoutContainer()));
    if (size.greaterThan(LayoutUtils.getCurrentSize(orientation, getLayoutContainer()))) {
      grid.setOverflow(true, orientation);
    }
  }

  public void postProcessing(Orientation orientation) {

    if (!getLayoutContainer().isLayoutChildren()) {
      return;
    }

    final BankHead[] heads = grid.getBankHeads(orientation);
    final BankHead[] heads2 = grid.getBankHeads(orientation.other());

    // call manage sizes for all sub-layout-managers
    for (int i = 0; i < heads.length; i++) {
      for (int j = 0; j < heads2.length; j++) {
        Cell cell = grid.getCell(i, j, orientation);
        if (cell instanceof OriginCell) {
          LayoutComponent component = cell.getComponent();

          component.setDisplay(Display.BLOCK);

          // compute the position of the cell
          Measure position = Measure.ZERO;
          position = position.add(LayoutUtils.getPaddingBegin(orientation, getLayoutContainer()));
          position = position.add(getMarginBegin(orientation));
          for (int k = 0; k < i; k++) {
            if (heads[k] != null
                && heads[k].getCurrent() != null
                && heads[k].isRendered()
                && heads[k].getCurrent().greaterThan(Measure.ZERO)) {
              position = position.add(heads[k].getCurrent());
              position = position.add(getSpacing(orientation));
            }
          }
          if (orientation == Orientation.HORIZONTAL) {
            component.setLeft(position);
          } else {
            component.setTop(position);
          }

          // call sub layout manager
          if (component instanceof LayoutContainer && (component.isRendered() || isRigid())) {
            ((LayoutContainer) component).getLayoutManager().postProcessing(orientation);
          }

          // set scrolling type
          final boolean scroll = grid.isOverflow(orientation);
          if (orientation == Orientation.HORIZONTAL) {
            getLayoutContainer().setOverflowX(scroll);
          } else {
            getLayoutContainer().setOverflowY(scroll);
          }

          // todo: optimize: the AutoLayoutTokens with columnSpan=1 are already called
        }
      }
    }
  }

  private LayoutContainer getLayoutContainer() {
    // todo: check with instanceof and do something in the error case
    return ((LayoutContainer) getParent());
  }

  public Measure getSpacing(Orientation orientation) {
    return orientation == Orientation.HORIZONTAL ? getColumnSpacing() : getRowSpacing();
  }

  public Measure getMarginBegin(Orientation orientation) {
    return orientation == Orientation.HORIZONTAL ? getMarginLeft() : getMarginTop();
  }

  public Measure getMarginEnd(Orientation orientation) {
    return orientation == Orientation.HORIZONTAL ? getMarginRight() : getMarginBottom();
  }

  /**
   * Compute the sum of the space between the cells.
   * There is one "space" less than cells that are not void.
   */
  private Measure computeSpacing(Orientation orientation, int startIndex, int length) {

    final BankHead[] heads = grid.getBankHeads(orientation);

    int count = 0;
    for (int i = startIndex; i < startIndex + length; i++) {
      if ((heads[i].isRendered())
          && (heads[i].getCurrent() == null || heads[i].getCurrent().greaterThan(Measure.ZERO))) {
        count++;
      }
    }
    if (count > 0) {
      return getSpacing(orientation).multiply(count - 1);
    } else {
      return Measure.ZERO;
    }
  }

  public abstract String getRows();

  public abstract void setRows(String rows);

  public abstract String getColumns();

  public abstract void setColumns(String columns);

  @Deprecated
  public abstract Measure getCellspacing();

  public abstract Measure getRowSpacing();

  public abstract Measure getColumnSpacing();

  public abstract Measure getMarginLeft();

  public abstract Measure getMarginTop();

  public abstract Measure getMarginRight();

  public abstract Measure getMarginBottom();

  public abstract boolean isColumnOverflow();

  public abstract boolean isRowOverflow();

  public abstract boolean isRigid();

  @Override
  public boolean getRendersChildren() {
    return false;
  }

  public String toString(int depth) {
    StringBuilder builder = new StringBuilder();
    builder.append(getClass().getSimpleName()).append("#");
    builder.append(getClientId(FacesContext.getCurrentInstance()));
    builder.append("\n");
    if (grid != null) {
      builder.append(StringUtils.repeat("  ", depth + 4));
      builder.append("horiz.: ");
      BankHead[] heads = grid.getBankHeads(Orientation.HORIZONTAL);
      for (int i = 0; i < heads.length; i++) {
        if (i != 0) {
          builder.append(StringUtils.repeat("  ", depth + 4 + 4));
        }
        builder.append(heads[i]);
        builder.append("\n");
      }
      builder.append(StringUtils.repeat("  ", depth + 4));
      builder.append("verti.: ");
      heads = grid.getBankHeads(Orientation.VERTICAL);
      for (int i = 0; i < heads.length; i++) {
        if (i != 0) {
          builder.append(StringUtils.repeat("  ", depth + 4 + 4));
        }
        builder.append(heads[i]);
        builder.append("\n");
      }
    }
    builder.setLength(builder.length() - 1);
    return builder.toString();
  }

  @Override
  public String toString() {
    return toString(0);
  }
}
