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

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
import org.noggit.CharArr;
import org.noggit.JSONWriter;

/**
 * This class encapsulates the config overlay json file. It is immutable
 * and any edit operations performed on tbhis gives a new copy of the object
 * with the changed value
 */
public class ConfigOverlay implements MapSerializable {
  private final int znodeVersion;
  private final Map<String, Object> data;
  private Map<String, Object> props;
  private Map<String, Object> userProps;

  public ConfigOverlay(Map<String, Object> jsonObj, int znodeVersion) {
    if (jsonObj == null) jsonObj = Collections.EMPTY_MAP;
    this.znodeVersion = znodeVersion;
    data = Collections.unmodifiableMap(jsonObj);
    props = (Map<String, Object>) data.get("props");
    if (props == null) props = Collections.EMPTY_MAP;
    userProps = (Map<String, Object>) data.get("userProps");
    if (userProps == null) userProps = Collections.EMPTY_MAP;
  }

  public Object getXPathProperty(String xpath) {
    return getXPathProperty(xpath, true);
  }

  public Object getXPathProperty(String xpath, boolean onlyPrimitive) {
    List<String> hierarchy = checkEditable(xpath, true, false);
    if (hierarchy == null) return null;
    return Utils.getObjectByPath(props, onlyPrimitive, hierarchy);
  }

  public ConfigOverlay setUserProperty(String key, Object val) {
    Map copy = new LinkedHashMap(userProps);
    copy.put(key, val);
    Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
    jsonObj.put("userProps", copy);
    return new ConfigOverlay(jsonObj, znodeVersion);
  }

  public ConfigOverlay unsetUserProperty(String key) {
    if (!userProps.containsKey(key)) return this;
    Map copy = new LinkedHashMap(userProps);
    copy.remove(key);
    Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
    jsonObj.put("userProps", copy);
    return new ConfigOverlay(jsonObj, znodeVersion);
  }

  public ConfigOverlay setProperty(String name, Object val) {
    List<String> hierarchy = checkEditable(name, false, true);
    Map deepCopy = (Map) Utils.fromJSON(Utils.toJSON(props));
    Map obj = deepCopy;
    for (int i = 0; i < hierarchy.size(); i++) {
      String s = hierarchy.get(i);
      if (i < hierarchy.size() - 1) {
        if (obj.get(s) == null || (!(obj.get(s) instanceof Map))) {
          obj.put(s, new LinkedHashMap<>());
        }
        obj = (Map) obj.get(s);
      } else {
        obj.put(s, val);
      }
    }

    Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
    jsonObj.put("props", deepCopy);

    return new ConfigOverlay(jsonObj, znodeVersion);
  }


  public static final String NOT_EDITABLE = "''{0}'' is not an editable property";

  private List<String> checkEditable(String propName, boolean isXPath, boolean failOnError) {
    LinkedList<String> hierarchy = new LinkedList<>();
    if (!isEditableProp(propName, isXPath, hierarchy)) {
      if (failOnError)
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, StrUtils.formatString(NOT_EDITABLE, propName));
      else return null;
    }
    return hierarchy;

  }

  public ConfigOverlay unsetProperty(String name) {
    List<String> hierarchy = checkEditable(name, false, true);
    Map deepCopy = (Map) Utils.fromJSON(Utils.toJSON(props));
    Map obj = deepCopy;
    for (int i = 0; i < hierarchy.size(); i++) {
      String s = hierarchy.get(i);
      if (i < hierarchy.size() - 1) {
        if (obj.get(s) == null || (!(obj.get(s) instanceof Map))) {
          return this;
        }
        obj = (Map) obj.get(s);
      } else {
        obj.remove(s);
      }
    }

    Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
    jsonObj.put("props", deepCopy);

    return new ConfigOverlay(jsonObj, znodeVersion);
  }

  public byte[] toByteArray() {
    return Utils.toJSON(data);
  }


  public int getZnodeVersion() {
    return znodeVersion;
  }

  @Override
  public String toString() {
    CharArr out = new CharArr();
    try {
      new JSONWriter(out, 2).write(data);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    return out.toString();
  }


  public static final String RESOURCE_NAME = "configoverlay.json";

  /*private static final Long STR_ATTR = 0L;
  private static final Long STR_NODE = 1L;
  private static final Long BOOL_ATTR = 10L;
  private static final Long BOOL_NODE = 11L;
  private static final Long INT_ATTR = 20L;
  private static final Long INT_NODE = 21L;
  private static final Long FLOAT_ATTR = 30L;
  private static final Long FLOAT_NODE = 31L;*/
  //The path maps to the xml xpath and value of 1 means it is a tag with a string value and value
  // of 0 means it is an attribute with string value

  private static Map editable_prop_map = (Map) Utils.fromJSONResource("EditableSolrConfigAttributes.json");

  public static boolean isEditableProp(String path, boolean isXpath, List<String> hierarchy) {
    return !(checkEditable(path, isXpath, hierarchy) == null);
  }


  public static Class checkEditable(String path, boolean isXpath, List<String> hierarchy) {
    List<String> parts = StrUtils.splitSmart(path, isXpath ? '/' : '.');
    Object obj = editable_prop_map;
    for (int i = 0; i < parts.size(); i++) {
      String part = parts.get(i);
      boolean isAttr = isXpath && part.startsWith("@");
      if (isAttr) {
        part = part.substring(1);
      }
      if (hierarchy != null) hierarchy.add(part);
      if (obj == null) return null;
      if (i == parts.size() - 1) {
        if (obj instanceof Map) {
          Map map = (Map) obj;
          Object o = map.get(part);
          return checkType(o, isXpath, isAttr);
        }
        return null;
      }
      obj = ((Map) obj).get(part);
    }
    return null;
  }

  static Class[] types = new Class[]{String.class, Boolean.class, Integer.class, Float.class};

  private static Class checkType(Object o, boolean isXpath, boolean isAttr) {
    if (o instanceof Long) {
      Long aLong = (Long) o;
      int ten = aLong.intValue() / 10;
      int one = aLong.intValue() % 10;
      if (isXpath && isAttr && one != 0) return null;
      return types[ten];
    } else {
      return null;
    }
  }

  public Map<String, String> getEditableSubProperties(String xpath) {
    Object o = Utils.getObjectByPath(props, false, StrUtils.splitSmart(xpath, '/'));
    if (o instanceof Map) {
      return (Map) o;
    } else {
      return null;
    }
  }

  public Map<String, Object> getUserProps() {
    return userProps;
  }

  @Override
  public Map<String, Object> toMap(Map<String, Object> map) {
    map.put(ZNODEVER, znodeVersion);
    map.putAll(data);
    return map;
  }

  public Map<String, Map> getNamedPlugins(String typ) {
    Map<String, Map> reqHandlers = (Map<String, Map>) data.get(typ);
    if (reqHandlers == null) return Collections.EMPTY_MAP;
    return Collections.unmodifiableMap(reqHandlers);
  }


  public ConfigOverlay addNamedPlugin(Map<String, Object> info, String typ) {
    Map dataCopy = Utils.getDeepCopy(data, 4);
    Map reqHandler = (Map) dataCopy.get(typ);
    if (reqHandler == null) dataCopy.put(typ, reqHandler = new LinkedHashMap());
    reqHandler.put(info.get(CoreAdminParams.NAME), info);
    return new ConfigOverlay(dataCopy, this.znodeVersion);
  }

  public ConfigOverlay deleteNamedPlugin(String name, String typ) {
    Map dataCopy = Utils.getDeepCopy(data, 4);
    Map reqHandler = (Map) dataCopy.get(typ);
    if (reqHandler == null) return this;
    reqHandler.remove(name);
    return new ConfigOverlay(dataCopy, this.znodeVersion);

  }

  public static final String ZNODEVER = "znodeVersion";
  public static final String NAME = "overlay";

  public static void main(String[] args) {
    System.out.println(Utils.toJSONString(editable_prop_map));
  }

}
