/*
 * Copyright 2003-2016 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 jetbrains.mps.nodeEditor.selection;

import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.nodeEditor.cells.DefaultCellInfo;
import jetbrains.mps.openapi.editor.EditorComponent;
import jetbrains.mps.openapi.editor.cells.CellInfo;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionInfo;
import jetbrains.mps.openapi.editor.selection.SelectionStoreException;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.ModuleRepositoryFacade;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class SelectionInfoImpl implements SelectionInfo {
  private static final Logger LOG = LogManager.getLogger(SelectionInfoImpl.class);

  private static final String CLASS_NAME_ATTRIBUTE = "className";
  private static final String MODULE_ID_ATTRIBUTE = "moduleID";
  private static final String CELL_INFO_ELEMENT_NAME = "cellInfo";
  private static final String PROPERTY_ELEMENT_NAME = "property";
  private static final String PROPERTY_NAME_ATTRIBUTE = "name";
  private static final String PROPERTY_VALUE_ATTRIBUTE = "value";


  private String mySelectionClassName;
  private String myModuleID;
  private Map<String, String> myProperties = new HashMap<String, String>();
  private DefaultCellInfo myCellInfo = null;

  public SelectionInfoImpl(Element element) {
    mySelectionClassName = element.getAttributeValue(CLASS_NAME_ATTRIBUTE);
    myModuleID = element.getAttributeValue(MODULE_ID_ATTRIBUTE);
    for (Object childElement : element.getChildren(PROPERTY_ELEMENT_NAME)) {
      Element entry = (Element) childElement;
      String name = entry.getAttributeValue(PROPERTY_NAME_ATTRIBUTE);
      assert name != null;
      String value = entry.getAttributeValue(PROPERTY_VALUE_ATTRIBUTE);
      assert value != null;
      myProperties.put(name, value);
    }
    Element cellInfoElement = element.getChild(CELL_INFO_ELEMENT_NAME);
    if (cellInfoElement != null) {
      myCellInfo = DefaultCellInfo.loadFrom(cellInfoElement);
    }
  }

  public SelectionInfoImpl(@NotNull String selectionClassName, @Nullable SModuleReference moduleID) {
    this(selectionClassName);
    myModuleID = moduleID == null ? null : PersistenceFacade.getInstance().asString(moduleID);
  }

  public SelectionInfoImpl(@NotNull String selectionClassName) {
    mySelectionClassName = selectionClassName;
  }

  public Map<String, String> getPropertiesMap() {
    return myProperties;
  }

  public void setCellInfo(CellInfo cellInfo) throws SelectionStoreException {
    if (!(cellInfo instanceof DefaultCellInfo)) {
      throw new SelectionStoreException("CellInfo is different from DefaultCellInfo: " + cellInfo);
    }
    myCellInfo = (DefaultCellInfo) cellInfo;
  }

  @Override
  public Selection createSelection(EditorComponent editorComponent) {
    try {
      Class<?> selectionClass;
      if (myModuleID != null) {
        SRepository repo = editorComponent.getEditorContext().getRepository();
        // XXX I have no idea whether there's model read access when #createSelection is invoked, rather take one.
        ReloadableModule reloadableModule = new ModelAccessHelper(repo).runReadAction(() -> {
          SModule module;
          try {
            SModuleReference mr = PersistenceFacade.getInstance().createModuleReference(myModuleID);
            module = mr.resolve(repo);
          } catch (IllegalArgumentException ex) {
            // fallback, perhaps, it's an old selection, where just module name has been stored
            // TODO remove this fallback once 3.5 is out
            module = new ModuleRepositoryFacade(repo).getModuleByName(myModuleID);
          }
          if (module == null) {
            LOG.error("Specified selection class module was not found by ID: " + myModuleID);
            return null;
          }
          if (!(module instanceof ReloadableModule)) {
            LOG.error(String.format("Module %s of specified selection class (%s) can not load classes", myModuleID, mySelectionClassName));
            return null;
          }
          return (ReloadableModule) module;
        });
        if (reloadableModule == null) {
          return null;
        }
        // I know it's odd to access module outside ot model read (although the module is likely deployed and shall not
        // get disposed unexpectedly). Just don't want to refactor the a lot (exception handling for both if/else cases).
        selectionClass = reloadableModule.getClass(mySelectionClassName);
      } else {
        selectionClass = getClass().getClassLoader().loadClass(mySelectionClassName);
      }
      if (!Selection.class.isAssignableFrom(selectionClass)) {
        LOG.error("Serialized selection class: " + mySelectionClassName + " is not a subclass of " + Selection.class.getName());
        return null;
      }
      Constructor<Selection> constructor = ((Class<Selection>) selectionClass).getConstructor(EditorComponent.class, Map.class, CellInfo.class);
      return constructor.newInstance(editorComponent, myProperties, myCellInfo);
    } catch (ClassNotFoundException | NoSuchMethodException e) {
      LOG.error(null, e);
      return null;
    } catch (InvocationTargetException e) {
      if (e.getTargetException() instanceof SelectionRestoreException) {
        /**
         * Skipping this exception because it is indicating that selection was not restored due to some changes in
         * associated model(s).
         *
         * For example: element that was indicated as selected before is not in a model anymore (was deleter).
         */
        return null;
      }
      LOG.error(null, e);
      return null;
    } catch (InstantiationException e) {
      LOG.error(null, e);
      return null;
    } catch (IllegalAccessException e) {
      LOG.error(null, e);
      return null;
    }
  }

  public void persistToXML(Element element) {
    element.setAttribute(CLASS_NAME_ATTRIBUTE, mySelectionClassName);
    if (myModuleID != null) {
      element.setAttribute(MODULE_ID_ATTRIBUTE, myModuleID);
    }
    for (Entry<String, String> propertyEntry : myProperties.entrySet()) {
      Element propertyElement = new Element(PROPERTY_ELEMENT_NAME);
      propertyElement.setAttribute(PROPERTY_NAME_ATTRIBUTE, propertyEntry.getKey());
      propertyElement.setAttribute(PROPERTY_VALUE_ATTRIBUTE, propertyEntry.getValue());
      element.addContent(propertyElement);
    }
    if (myCellInfo != null) {
      Element cellInfoElement = new Element(CELL_INFO_ELEMENT_NAME);
      myCellInfo.saveTo(cellInfoElement);
      element.addContent(cellInfoElement);
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    SelectionInfoImpl that = (SelectionInfoImpl) o;

    if (!mySelectionClassName.equals(that.mySelectionClassName)) {
      return false;
    }
    if (myCellInfo != null ? !myCellInfo.equals(that.myCellInfo) : that.myCellInfo != null) {
      return false;
    }
    if (!myProperties.equals(that.myProperties)) {
      return false;
    }

    return true;
  }

  @Override
  public int hashCode() {
    int result = mySelectionClassName.hashCode();
    result = 31 * result + myProperties.hashCode();
    result = 31 * result + (myCellInfo != null ? myCellInfo.hashCode() : 0);
    return result;
  }

  public static class Util {
    public static int getIntProperty(Map<String, String> properties, String propertyName) throws SelectionStoreException {
      String propertyValue = properties.get(propertyName);
      if (propertyValue == null) {
        throw new SelectionStoreException("Cannot load int property - property value was not specified for propertyName = " + propertyName);
      }
      try {
        return Integer.parseInt(propertyValue);
      } catch (NumberFormatException e) {
        throw new SelectionStoreException("Unable to parse integer position value: " + propertyValue);
      }
    }

    public static boolean getBooleanProperty(Map<String, String> properties, String propertyName) throws SelectionStoreException {
      String propertyValue = properties.get(propertyName);
      if (propertyValue == null) {
        throw new SelectionStoreException("Cannot load boolean property - property value was not specified for propertyName = " + propertyName);
      }
      return Boolean.parseBoolean(propertyValue);
    }

    public static Enum getEnumProperty(Map<String, String> properties, String propertyName, Class<? extends Enum> enumClass, Enum defaultPropertyValue) throws
        SelectionStoreException {
      String propertyValue = properties.get(propertyName);
      if (propertyValue == null) {
        return defaultPropertyValue;
//        throw new SelectionStoreException("Cannot load enum property - property value was not specified for propertyName = " + propertyName);
      }
      try {
        return Enum.valueOf(enumClass, propertyValue);
      } catch (IllegalArgumentException ex) {
        throw new SelectionStoreException("Invalid enum literal name specified: " + propertyValue + " for enum: " + enumClass.getCanonicalName());
      }
    }
  }
}
