/*
 * 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.smodel;

import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.classloading.ModuleReloadListener;
import jetbrains.mps.components.CoreComponent;
import jetbrains.mps.kernel.model.SModelUtil;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.smodel.language.ConceptRegistryUtil;
import jetbrains.mps.smodel.legacy.ConceptMetaInfoConverter;
import jetbrains.mps.smodel.runtime.ConstraintsDescriptor;
import jetbrains.mps.smodel.runtime.PropertyConstraintsDescriptor;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.JavaNameUtil;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SDataType;
import org.jetbrains.mps.openapi.language.SPrimitiveDataType;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.model.SNode;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public abstract class PropertySupport {
  private static final Logger LOG = LogManager.getLogger(PropertySupport.class);
  private static PropertySupportCache ourPropertySupportCache;

  /**
   * new validation method
   */
  @Deprecated
  public boolean canSetValue(SNode node, String propertyName, String value, boolean nullsAlwaysAllowed) {
    SProperty sprop = ((ConceptMetaInfoConverter) node.getConcept()).convertProperty(propertyName);
    return canSetValue(node, sprop, value, nullsAlwaysAllowed);
  }

  public boolean canSetValue(SNode node, SProperty property, String value, boolean nullsAlwaysAllowed) {
    if (value == null && nullsAlwaysAllowed) {
      return true;  // can always remove property
    }
    if (value == null) {
      value = "";
    }
    if (!canSetValue(value)) {
      return false;
    }
    PropertyConstraintsDescriptor descriptor = getPropertyConstraintsDescriptor(node, property);
    if (descriptor == null) {
      LOG.error("No property constraints are available for property " + property.getName() + " in node " + node.getPresentation());
      return false;
    }
    return canSetValue(descriptor, node, property, value);
  }

  /*package*/
  static PropertyConstraintsDescriptor getPropertyConstraintsDescriptor(SNode node, SProperty property) {
    ConstraintsDescriptor constraintsDescriptor = ConceptRegistryUtil.getConstraintsDescriptor(node.getConcept());

    PropertyConstraintsDescriptor descriptor;
    descriptor = constraintsDescriptor.getProperty(property);
    return descriptor;
  }

  @Deprecated
  public boolean canSetValue(PropertyConstraintsDescriptor descriptor, SNode node, String propertyName, String value) {
    if (value == null) {
      value = "";
    }
    return descriptor.validateValue(node, value);
  }

  public boolean canSetValue(PropertyConstraintsDescriptor descriptor, SNode node, SProperty property, String value) {
    if (value == null) {
      value = "";
    }
    return descriptor.validateValue(node, value);
  }

  @Deprecated
  public boolean canSetValue(SNode node, String propertyName, String value) {
    return canSetValue(node, propertyName, value, true);
  }

  public boolean canSetValue(SNode node, SProperty property, String value) {
    return canSetValue(node, property, value, true);
  }

  /**
   * old validation method - keep it for compatibility
   */
  protected abstract boolean canSetValue(String value);

  public String toInternalValue(String value) {
    return value;
  }

  public String fromInternalValue(String value) {
    return value;
  }

  @NotNull
  public static PropertySupport getPropertySupport(@NotNull final SProperty property) {
    SDataType dataType = property.getType();
    if (dataType != null) {
      if (dataType instanceof SPrimitiveDataType) {
        switch (((SPrimitiveDataType) dataType).getType()) {
          case SPrimitiveDataType.BOOL:
            return BooleanPropertySupport.INSTANCE;
          case SPrimitiveDataType.INT:
            return IntegerPropertySupport.INSTANCE;
          case SPrimitiveDataType.STRING:
            return DefaultPropertySupport.INSTANCE;
          default:
            throw new RuntimeException("Unknown primitive type: " + dataType);
        }
      } else {
        //todo: get real prop support
        SNode declarationNode = property.getDeclarationNode();
        return declarationNode != null ? getPropertySupport(declarationNode) : DefaultPropertySupport.INSTANCE;
      }
    }
    return DefaultPropertySupport.INSTANCE;
  }

  @NotNull
  public static PropertySupport getPropertySupport(@NotNull final SNode propertyDeclaration) {
    return NodeReadAccessCasterInEditor.runReadTransparentAction(new Computable<PropertySupport>() {
      @Override
      public PropertySupport compute() {
        SNode dataType = SNodeUtil.getPropertyDeclaration_DataType(propertyDeclaration);
        if (dataType != null) {
          PropertySupport propertySupport = ourPropertySupportCache.get(dataType);
          if (propertySupport != null) {
            return propertySupport;
          }

          if (SNodeUtil.isInstanceOfPrimitiveDataTypeDeclaration(dataType)) {
            String dataTypeName = dataType.getName();
            if (Primitives.STRING_TYPE.equals(dataTypeName)) {
              propertySupport = new DefaultPropertySupport();
            } else if (Primitives.INTEGER_TYPE.equals(dataTypeName)) {
              propertySupport = new IntegerPropertySupport();
            } else if (Primitives.BOOLEAN_TYPE.equals(dataTypeName)) {
              propertySupport = new BooleanPropertySupport();
            } else {
              throw new RuntimeException("Unknown primitive type: " + dataTypeName);
            }
          } else {
            propertySupport = loadPropertySupport(propertyDeclaration);
          }

          if (propertySupport == null) {
            propertySupport = new DefaultPropertySupport();
          }
          ourPropertySupportCache.put(dataType, propertySupport);

          return propertySupport;
        }
        return new DefaultPropertySupport();
      }
    });
  }

  private static String getClassName(SNode propertyDataType) {
    return propertyDataType.getName() + "_PropertySupport";
  }

  private static PropertySupport loadPropertySupport(SNode propertyDeclaration) {
    SNode propertyDataType = SNodeUtil.getPropertyDeclaration_DataType(propertyDeclaration);
    Language l = SModelUtil.getDeclaringLanguage(propertyDataType);

    String propertySupportClassName = JavaNameUtil.fqClassName(propertyDataType.getModel(), getClassName(propertyDataType));
    PropertySupport propertySupport = null;
    try {
      Class propertySupportClass = ClassLoaderManager.getInstance().getOwnClass(l, propertySupportClassName);
      if (propertySupportClass != null) {
        Constructor constructor = propertySupportClass.getConstructor();
        propertySupport = (PropertySupport) constructor.newInstance();
      } else {
        LOG.error("Can't find a class " + propertySupportClassName + " using classloader of " + l.getModuleName() + " module");
      }
    } catch (NoSuchMethodException e) {
      LOG.error(null, e);
    } catch (InstantiationException e) {
      LOG.error(null, e);
    } catch (IllegalAccessException e) {
      LOG.error(null, e);
    } catch (InvocationTargetException e) {
      LOG.error(null, e);
    }
    return propertySupport;
  }

  private static class DefaultPropertySupport extends PropertySupport {
    public static DefaultPropertySupport INSTANCE = new DefaultPropertySupport();

    @Override
    public boolean canSetValue(String value) {
      return true;
    }
  }

  private static class IntegerPropertySupport extends PropertySupport {
    public static IntegerPropertySupport INSTANCE = new IntegerPropertySupport();

    @Override
    public boolean canSetValue(String value) {
      if (value != null && value.startsWith("+")) {
        return false;
      }
      try {
        Integer.parseInt(value);
        return true;
      } catch (NumberFormatException e) {
        // it is OK
      }
      return false;
    }
  }

  private static class BooleanPropertySupport extends PropertySupport {
    public static BooleanPropertySupport INSTANCE = new BooleanPropertySupport();

    @Override
    public boolean canSetValue(String value) {
      return String.valueOf(value).equals("true") || String.valueOf(value).equals("false");
    }

    @Override
    public String fromInternalValue(String value) {
      if ("true".equals(value)) {
        return value;
      }
      return "false";
    }

    @Override
    public String toInternalValue(String value) {
      if ("true".equals(value)) {
        return value;
      }
      return null;
    }
  }

  /**
   * TODO: remove in 3.3, replace with some generated code, probably in the StructureAspectDescriptor.
   * Preserving the cache, but starting to listen to the reloading events to keep the cache up-to-date
   *
   * @deprecated
   */
  @Deprecated
  public static class PropertySupportCache implements CoreComponent, ModuleReloadListener {
    private final Map<SNode, PropertySupport> myMap = new ConcurrentHashMap<SNode, PropertySupport>();
    private final ClassLoaderManager myCLM;

    public PropertySupportCache(ClassLoaderManager clm) {
      myCLM = clm;
    }

    public void put(SNode key, PropertySupport cache) {
      myMap.put(key, cache);
    }

    public PropertySupport get(SNode key) {
      return myMap.get(key);
    }

    @Override
    public void modulesReloaded(Set<ReloadableModule> modules) {
      myMap.clear();
    }

    @Override
    public void init() {
      myCLM.addReloadListener(this);
      PropertySupport.ourPropertySupportCache = this;
    }

    @Override
    public void dispose() {
      myCLM.removeReloadListener(this);
    }
  }
}
