/*
 * Copyright 2013 Google Inc.
 *
 * 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.google.gwt.core.ext.soyc.coderef;

import com.google.gwt.core.ext.soyc.coderef.EntityDescriptor.Fragment;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.json.JSONArray;
import com.google.gwt.thirdparty.json.JSONException;
import com.google.gwt.thirdparty.json.JSONObject;

import java.util.Map;

/**
 * Serialize/Deserialize EntityDescriptor instances to/from json.
 *
 */
public class EntityDescriptorJsonTranslator {

  private static class Deserializer {

    private final Map<Integer, MethodDescriptor> mapMethods = Maps.newHashMap();
    private final Map<MethodDescriptor, JSONArray> mapDependants = Maps.newIdentityHashMap();

    private PackageDescriptor readJson(JSONObject jsonObject) throws JSONException {
      String packageName = jsonObject.getString(ENTITY_NAME);
      PackageDescriptor packageDescriptor = readJsonPackage(jsonObject, packageName,
          packageName.equals(PackageDescriptor.DEFAULT_PKG) ? "" : packageName);
      setMethodDependencies();
      return packageDescriptor;
    }

    private void setMethodDependencies() throws JSONException {
      for (MethodDescriptor method : mapDependants.keySet()) {
        JSONArray dependants = mapDependants.get(method);
        for (int i = 0; i < dependants.length(); i++) {
          method.addDependant(mapMethods.get(dependants.getInt(i)));
        }
      }
    }

    private PackageDescriptor readJsonPackage(JSONObject jsonObject, String name,
        String longName) throws JSONException {
      PackageDescriptor descriptor = new PackageDescriptor(name, longName);
      JSONArray clss = jsonObject.getJSONArray(CLASSES);
      for (int i = 0; i < clss.length(); i++) {
        descriptor.addClass(readJsonClass(clss.getJSONObject(i), longName));
      }
      JSONArray packages = jsonObject.getJSONArray(PACKAGES);
      for (int i = 0; i < packages.length(); i++) {
        JSONObject subPackage = packages.getJSONObject(i);
        String packageName = subPackage.getString(ENTITY_NAME);
        descriptor.addPackage(readJsonPackage(subPackage, packageName,
            longName + (longName.isEmpty() ? "" : ".") + packageName));
      }
      return descriptor;
    }

    private ClassDescriptor readJsonClass(JSONObject jsonObject, String packageName)
        throws JSONException {
      ClassDescriptor descriptor = new ClassDescriptor(jsonObject.getString("name"), packageName);
      updateEntity(descriptor, jsonObject);
      JSONArray fields = jsonObject.getJSONArray(FIELDS);
      for (int i = 0; i < fields.length(); i++) {
        descriptor.addField(readJsonField(fields.getJSONObject(i), descriptor));
      }
      JSONArray methods = jsonObject.getJSONArray(METHODS);
      for (int i = 0; i < methods.length(); i++) {
        descriptor.addMethod(readJsonMethod(methods.getJSONObject(i), descriptor));
      }
      return descriptor;
    }

    private MethodDescriptor readJsonMethod(JSONObject jsonObject, ClassDescriptor classDescriptor)
        throws JSONException {
      MethodDescriptor method = new MethodDescriptor(classDescriptor,
          jsonObject.getString(ENTITY_NAME));
      updateEntity(method, jsonObject);
      method.setUniqueId(jsonObject.getInt(METHOD_ID));

      mapMethods.put(method.getUniqueId(), method);
      mapDependants.put(method, jsonObject.getJSONArray(METHOD_DEPENDENTS));

      return method;
    }

    private FieldDescriptor readJsonField(JSONObject jsonObject, ClassDescriptor classDescriptor)
        throws JSONException {
      String[] fullName = jsonObject.getString(ENTITY_NAME).split(":");
      FieldDescriptor fieldDescriptor = new FieldDescriptor(classDescriptor, fullName[0], fullName[1]);
      updateEntity(fieldDescriptor, jsonObject);
      return fieldDescriptor;
    }

    private void updateEntity(EntityDescriptor entity, JSONObject jsonObject) throws JSONException {
      JSONArray jsNames = jsonObject.getJSONArray(ENTITY_JS);
      for (int i = 0; i < jsNames.length(); i++) {
        entity.addObfuscatedName(jsNames.getString(i));
      }
      JSONArray frags = jsonObject.getJSONArray(EntityRecorder.FRAGMENTS);
      for (int i = 0; i < frags.length(); i++) {
        JSONObject frag = frags.getJSONObject(i);
        entity.addFragment(
            new Fragment(frag.getInt(EntityRecorder.FRAGMENT_ID),
                frag.getInt(EntityRecorder.FRAGMENT_SIZE)));
      }
    }
  }

  public static final String ENTITY_JS         = "js";
  public static final String ENTITY_NAME       = "name";
  public static final String FIELDS            = "fields";
  public static final String METHOD_ID         = "id";
  public static final String METHOD_DEPENDENTS = "dependents";
  public static final String METHODS           = "methods";
  public static final String CLASSES           = "classes";
  public static final String PACKAGES          = "packages";

  private static JSONObject writeJsonFromEntity(EntityDescriptor entity) throws JSONException {
    JSONObject json = new JSONObject();
    JSONArray fragments = new JSONArray();
    for (EntityDescriptor.Fragment frg : entity.getFragments()) {
      JSONObject frag = new JSONObject();
      frag.put(EntityRecorder.FRAGMENT_ID, frg.getId());
      frag.put(EntityRecorder.FRAGMENT_SIZE, frg.getSize());

      fragments.put(frag);
    }
    json.put(EntityRecorder.FRAGMENTS, fragments);
    json.put(ENTITY_JS, new JSONArray(entity.getObfuscatedNames()));
    return json;
  }

  private static JSONObject writeJsonFromMember(MemberDescriptor entity) throws JSONException {
    JSONObject json = writeJsonFromEntity(entity);
    json.put(ENTITY_NAME, entity.getJsniSignature());
    return json;
  }

  public static JSONObject writeJson(PackageDescriptor pkg) throws JSONException {
    JSONObject json = new JSONObject();
    json.put(ENTITY_NAME, pkg.getName());
    // classes
    JSONArray classes = new JSONArray();
    for (ClassDescriptor classDescriptor : pkg.getClasses()) {
      JSONObject jsonClass = writeJsonFromEntity(classDescriptor);
      jsonClass.put(ENTITY_NAME, classDescriptor.getName());
      // fields
      JSONArray fields = new JSONArray();
      for (FieldDescriptor fieldDescriptor : classDescriptor.getFields()) {
        fields.put(writeJsonFromMember(fieldDescriptor));
      }
      jsonClass.put(FIELDS, fields);
      // methods
      JSONArray methods = new JSONArray();
      for (MethodDescriptor methodDescriptor : classDescriptor.getMethods()) {
        JSONObject jsonMethod = writeJsonFromMember(methodDescriptor);
        jsonMethod.put(METHOD_ID, methodDescriptor.getUniqueId());
        jsonMethod.put(METHOD_DEPENDENTS, new JSONArray(methodDescriptor.getDependentPointers()));
        methods.put(jsonMethod);
      }
      jsonClass.put(METHODS, methods);

      classes.put(jsonClass);
    }
    json.put(CLASSES, classes);
    // packages
    JSONArray packages = new JSONArray();
    for (PackageDescriptor packageDescriptor : pkg.getPackages()) {
      packages.put(writeJson(packageDescriptor));
    }
    json.put(PACKAGES, packages);

    return json;
  }

  public static PackageDescriptor readJson(JSONObject jsonObject) throws JSONException {
    return new Deserializer().readJson(jsonObject);
  }

  private EntityDescriptorJsonTranslator() { }
}
