/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed 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 com.intellij.application.options.codeStyle.arrangement.match;

import com.intellij.application.options.codeStyle.arrangement.ArrangementConstants;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementStandardSettingsManager;
import com.intellij.application.options.codeStyle.arrangement.animation.ArrangementAnimationManager;
import com.intellij.application.options.codeStyle.arrangement.animation.ArrangementAnimationPanel;
import com.intellij.application.options.codeStyle.arrangement.color.ArrangementColorsProvider;
import com.intellij.application.options.codeStyle.arrangement.component.ArrangementAndMatchConditionComponent;
import com.intellij.application.options.codeStyle.arrangement.component.ArrangementAtomMatchConditionComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementEntryMatcher;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementAtomMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementCompositeMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchConditionVisitor;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementUiComponent;
import com.intellij.util.Consumer;
import org.jetbrains.annotations.NotNull;

import java.util.Set;

/**
 * @author Denis Zhdanov
 * @since 8/10/12 2:53 PM
 */
public class ArrangementMatchNodeComponentFactory {

  private static final Logger LOG = Logger.getInstance(ArrangementMatchNodeComponentFactory.class);

  @NotNull private final ArrangementStandardSettingsManager mySettingsManager;
  @NotNull private final ArrangementColorsProvider          myColorsProvider;
  @NotNull private final ArrangementMatchingRulesControl    myList;

  public ArrangementMatchNodeComponentFactory(@NotNull ArrangementStandardSettingsManager manager,
                                              @NotNull ArrangementColorsProvider provider,
                                              @NotNull ArrangementMatchingRulesControl list)
  {
    mySettingsManager = manager;
    myColorsProvider = provider;
    myList = list;
  }

  /**
   * Allows to build UI component for the given model.
   *
   * @param rendererTarget      target model element for which UI component should be built
   * @param rule                rule which contains given 'renderer target' condition and serves as
   *                            a data entry for the target list model
   * @param allowModification   flag which indicates whether given model can be changed at future
   * @return renderer for the given model
   */
  @NotNull
  public ArrangementUiComponent getComponent(@NotNull final ArrangementMatchCondition rendererTarget,
                                             @NotNull final StdArrangementMatchRule rule,
                                             final boolean allowModification)
  {
    final Ref<ArrangementUiComponent> ref = new Ref<>();
    rendererTarget.invite(new ArrangementMatchConditionVisitor() {
      @Override
      public void visit(@NotNull ArrangementAtomMatchCondition condition) {
        RemoveAtomConditionCallback callback = allowModification ? new RemoveAtomConditionCallback(rule) : null;
        ArrangementUiComponent component = new ArrangementAtomMatchConditionComponent(
          mySettingsManager, myColorsProvider, condition, callback
        );
        ref.set(component);
      }

      @Override
      public void visit(@NotNull ArrangementCompositeMatchCondition condition) {
        ref.set(new ArrangementAndMatchConditionComponent(rule, condition, ArrangementMatchNodeComponentFactory.this, mySettingsManager, allowModification));
      }
    });
    return ref.get();
  }

  private class RemoveAtomConditionCallback implements Consumer<ArrangementAtomMatchConditionComponent>,
                                                       ArrangementAnimationManager.Callback
  {

    @NotNull private final StdArrangementMatchRule myRule;

    @NotNull private Object myModelValue;
    private int myRow;

    RemoveAtomConditionCallback(@NotNull StdArrangementMatchRule rule) {
      myRule = rule;
      myModelValue = myRule;
    }

    @Override
    public void consume(@NotNull ArrangementAtomMatchConditionComponent component) {
      ArrangementAtomMatchCondition condition = component.getMatchCondition();
      ArrangementMatchingRulesModel model = myList.getModel();
      int i = getModelIndex();
      if (i < 0) {
        return;
      }
      myRow = i;

      ArrangementMatchCondition existingCondition = myRule.getMatcher().getCondition();
      if (existingCondition.equals(condition)) {
        // We can't just remove an element at this time because that breaks last row rendering. 
        model.set(i, myModelValue = new DummyElement());
      }
      else {
        assert existingCondition instanceof ArrangementCompositeMatchCondition;
        Set<ArrangementMatchCondition> operands = ((ArrangementCompositeMatchCondition)existingCondition).getOperands();
        operands.remove(condition);
        if (operands.isEmpty()) {
          // We can't just remove an element at this time because that breaks last row rendering.
          model.set(i, myModelValue = new DummyElement());
        }
        else if (operands.size() == 1) {
          myModelValue = new StdArrangementMatchRule(new StdArrangementEntryMatcher(operands.iterator().next()), myRule.getOrderType());
          model.set(i, myModelValue);
        }
        else if (ArrangementConstants.LOG_RULE_MODIFICATION) {
          LOG.info(String.format("Removed '%s' condition. Current rule state: %s", condition, myRule));
          myModelValue = myRule;
        }
      }

      ArrangementAnimationPanel panel = component.getAnimationPanel();
      new ArrangementAnimationManager(panel, this).startAnimation();
    }

    @Override
    public void onAnimationIteration(boolean finished) {
      refreshRow();
      if (myRow < 0) {
        return;
      }
      myList.repaintRows(myRow, myRow, finished);
      if (!finished) {
        return;
      }
      if (myModelValue instanceof DummyElement) {
        myList.removeRow(myRow);
      }
    }

    private void refreshRow() {
      ArrangementMatchingRulesModel model = myList.getModel();
      if (myRow < 0 || myRow >= model.getSize()) {
        myRow = getModelIndex();
      }
      else {
        Object o = model.getElementAt(myRow);
        if (o != myModelValue) {
          myRow = getModelIndex();
        }
      }
    }

    private int getModelIndex() {
      // We can't just use model.indexOf(myRule) because there is a possible case that the model contain equal
      // rules (rule1.equals(rule2) == true). That's why we have a helper method for search by reference identity. 
      ArrangementMatchingRulesModel model = myList.getModel();
      for (int i = 0, max = model.getSize(); i < max; i++) {
        if (model.getElementAt(i) == myModelValue) {
          return i;
        }
      }
      return -1;
    }
  }

  private static class DummyElement {
    @Override
    public String toString() {
      return "dummy-" + System.identityHashCode(this);
    }
  }
}
