// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk.internal.protocolparser.dynamicimpl;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A set of interfaces and classes used to generate Java code of parser implementation.
 */
public interface JavaCodeGenerator {

  GlobalScope newGlobalScope(Collection<TypeHandler<?>> typeHandlers,
      Collection<GeneratedCodeMap> basePackages);

  interface GlobalScope {
    String getTypeImplReference(TypeHandler<?> typeHandler);
    String getTypeImplShortName(TypeHandler<?> typeHandler);

    /**
     * @return new {@link FileScope} that extends {@link GlobalScope} and shares the state
     *     with this {@link GlobalScope}
     */
    FileScope newFileScope(StringBuilder output);
  }

  interface FileScope extends GlobalScope {
    StringBuilder getStringBuilder();

    void startLine(String line);
    void append(String line);

    void indentRight();
    void indentLeft();

    /**
     * @return new {@link ClassScope} that extends {@link FileScope} and shares the state
     *     with this {@link FileScope}
     */
    ClassScope newClassScope();
  }

  interface ClassScope extends FileScope {
    ClassScope getRootClassScope();

    /**
     * @return new {@link ClassScope} that has different state as {@link ClassScope},
     *     but shares the state with this as {@link FileScope}
     */
    @Override
    ClassScope newClassScope();

    /**
     * Adds a member to the class. The member is identified by the key. Member Java code
     * is generated later. If the member with a particular key
     * has already been added, method return data instance if returned the previous time.
     *
     * @return user-defined field element data
     */
    <T extends ElementData> T addMember(Object key, ElementFactory<T> factory);

    /**
     * @return new {@link MethodScope} that extends {@link ClassScope} and shares the state
     *     with this {@link ClassScope}.
     */
    MethodScope newMethodScope();

    /**
     * Writes Java code of all added members.
     */
    void writeClassMembers();
  }

  interface MethodScope extends ClassScope {
    /**
     * @return a name unique to this scope with the provided prefix
     */
    String newMethodScopedName(String prefix);
  }

  interface ElementData {
    void generateCode(ClassScope classScope);
  }

  interface ElementFactory<T extends ElementData> {
    T create(int code);
  }

  class Util {
    /**
     * Generate Java type name of the passed type. Type may be parameterized.
     */
    public static void writeJavaTypeName(Type arg, StringBuilder output) {
      if (arg instanceof Class) {
        Class<?> clazz = (Class<?>) arg;
        output.append(clazz.getCanonicalName());
      } else if (arg instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) arg;
        writeJavaTypeName(parameterizedType.getRawType(), output);
        output.append("<");
        Type[] params = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < params.length; i++) {
          if (i != 0) {
            output.append(", ");
          }
          writeJavaTypeName(params[i], output);
        }
        output.append(">");
      } else if (arg instanceof WildcardType) {
        WildcardType wildcardType = (WildcardType) arg;
        Type[] upperBounds = wildcardType.getUpperBounds();
        if (upperBounds == null) {
          throw new RuntimeException();
        }
        if (upperBounds.length != 1) {
          throw new RuntimeException();
        }
        output.append("? extends ");
        writeJavaTypeName(upperBounds[0], output);
      } else {
        output.append(arg);
      }
    }

    /**
     * Generates a commonly-used code that gets property from JSON in form of
     * 'value' and 'hasValue' pair of variables.
     */
    public static void writeReadValueAndHasValue(MethodScope scope, String fieldName,
        String jsonObjectRef, String valueRef, String hasValueRef) {
      scope.startLine("Object " + valueRef + " = " + jsonObjectRef + ".get(\"" +
          fieldName + "\");\n");
      scope.startLine("boolean " + hasValueRef + ";\n");
      scope.startLine("if (" + valueRef + " == null) {\n");
      scope.startLine("  " + hasValueRef + " = " + jsonObjectRef + ".containsKey(\"" +
          fieldName + "\");\n");
      scope.startLine("} else {\n");
      scope.startLine("  " + hasValueRef + " = true;\n");
      scope.startLine("}\n");
    }

    public static final String BASE_PACKAGE = "org.chromium.sdk.internal.protocolparser";
    public static final String THROWS_CLAUSE =
        " throws org.chromium.sdk.internal.protocolparser.JsonProtocolParseException";
  }


  class Impl implements JavaCodeGenerator {

    @Override
    public GlobalScope newGlobalScope(Collection<TypeHandler<?>> typeHandlers,
        Collection<GeneratedCodeMap> basePackages) {
      return new GlobalScopeImpl(typeHandlers, basePackages);
    }

    private static class GlobalScopeImpl implements GlobalScope {
      private final State state;

      GlobalScopeImpl(Collection<TypeHandler<?>> typeHandlers,
          Collection<GeneratedCodeMap> basePackages) {
        state = new State(typeHandlers, basePackages);
      }

      GlobalScopeImpl(GlobalScopeImpl globalScopeImpl) {
        state = globalScopeImpl.state;
      }

      @Override
      public String getTypeImplReference(TypeHandler<?> typeHandler) {
        return state.getTypeImplReference(typeHandler);
      }

      @Override
      public String getTypeImplShortName(TypeHandler<?> typeHandler) {
        return state.getTypeImplShortName(typeHandler);
      }

      @Override
      public FileScope newFileScope(StringBuilder output) {
        return new FileScopeImpl(this, output);
      }

      private static class State {
        private final Map<TypeHandler<?>, String> type2Name;
        private final Collection<GeneratedCodeMap> basePackages;

        State(Collection<TypeHandler<?>> typeHandlers, Collection<GeneratedCodeMap> basePackages) {
          this.basePackages = basePackages;
          type2Name = buildLocalTypeNameMap(typeHandlers);
        }

        String getTypeImplReference(TypeHandler<?> typeHandler) {
          String localName = type2Name.get(typeHandler);
          if (localName == null) {
            for (GeneratedCodeMap base : basePackages) {
              String result = base.getTypeImplementationReference(typeHandler.getTypeClass());
              if (result != null) {
                return result;
              }
            }
          } else {
            return localName;
          }
          throw new RuntimeException();
        }

        String getTypeImplShortName(TypeHandler<?> typeHandler) {
          String result = type2Name.get(typeHandler);
          if (result == null) {
            throw new RuntimeException();
          }
          return result;
        }

        private static Map<TypeHandler<?>, String> buildLocalTypeNameMap(
            Collection<TypeHandler<?>> typeHandlers) {
          List<TypeHandler<?>> list = new ArrayList<TypeHandler<?>>(typeHandlers);

          // Sort to produce consistent GeneratedCodeMap later.
          Collections.sort(list, new Comparator<TypeHandler<?>>() {
            @Override
            public int compare(TypeHandler<?> o1, TypeHandler<?> o2) {
              return getName(o1).compareTo(getName(o2));
            }

            private String getName(TypeHandler<?> handler) {
              return handler.getTypeClass().getName();
            }
          });

          int uniqueCode = 0;
          Map<TypeHandler<?>, String> result = new HashMap<TypeHandler<?>, String>();

          for (TypeHandler<?> handler : list) {
            String name = "Value_" + uniqueCode++;
            Object conflict = result.put(handler, name);
            if (conflict != null) {
              throw new RuntimeException();
            }
          }
          return result;
        }
      }
    }

    private static class FileScopeImpl extends GlobalScopeImpl implements FileScope {
      private final State state;

      FileScopeImpl(GlobalScopeImpl globalScopeImpl, StringBuilder stringBuilder) {
        super(globalScopeImpl);
        this.state = new State(stringBuilder);
      }

      FileScopeImpl(FileScopeImpl fileScopeImpl) {
        super(fileScopeImpl);
        this.state = fileScopeImpl.state;
      }

      @Override
      public StringBuilder getStringBuilder() {
        return state.getStringBuilder();
      }

      @Override
      public void startLine(String line) {
        state.startLine(line);
      }

      @Override
      public void append(String line) {
        state.append(line);
      }

      @Override
      public void indentRight() {
        state.indentRight();
      }

      @Override
      public void indentLeft() {
        state.indentLeft();
      }

      @Override
      public ClassScope newClassScope() {
        return new ClassScopeImpl(this, asClassScopeImpl());
      }

      protected ClassScopeImpl asClassScopeImpl() {
        return null;
      }

      private static class State {
        private final StringBuilder stringBuilder;
        private int indent = 0;

        State(StringBuilder stringBuilder) {
          this.stringBuilder = stringBuilder;
        }

        StringBuilder getStringBuilder() {
          return stringBuilder;
        }

        void startLine(String line) {
          for (int i = 0; i < indent; i++) {
            stringBuilder.append(' ');
          }
          stringBuilder.append(line);
        }

        void append(String line) {
          stringBuilder.append(line);
        }

        void indentRight() {
          indent += 2;
        }

        void indentLeft() {
          indent -= 2;
        }
      }
    }

    private static class ClassScopeImpl extends FileScopeImpl implements ClassScope {
      private final State state;
      private final ClassScope parentClass;

      ClassScopeImpl(FileScopeImpl fileScopeImpl, ClassScope parentClass) {
        super(fileScopeImpl);
        this.state = new State();
        this.parentClass = parentClass;
      }

      ClassScopeImpl(ClassScopeImpl classScopeImpl) {
        super(classScopeImpl);
        this.state = classScopeImpl.state;
        this.parentClass = classScopeImpl.parentClass;
      }

      @Override
      public ClassScope getRootClassScope() {
        if (parentClass == null) {
          return this;
        } else {
          return parentClass.getRootClassScope();
        }
      }

      @Override
      public <T extends ElementData> T addMember(Object key,
          ElementFactory<T> factory) {
        return state.addMember(key, factory);
      }

      @Override
      public void writeClassMembers() {
        state.writeClassElements(this);
      }

      @Override
      public MethodScope newMethodScope() {
        return new MethodScopeImpl(this);
      }

      @Override
      protected ClassScopeImpl asClassScopeImpl() {
        return this;
      }

      private static class State {
        private final Map<Object, ElementData> key2ElementData =
            new HashMap<Object, JavaCodeGenerator.ElementData>(2);
        private int nextCode = 0;

        <T extends ElementData> T addMember(Object key, ElementFactory<T> factory) {
          List<Object> extendedKey = Arrays.asList(key, factory);
          ElementData rawData = key2ElementData.get(extendedKey);
          T data = (T) rawData;
          if (data == null) {
            data = factory.create(nextCode++);
            key2ElementData.put(extendedKey, data);
          }
          return data;
        }

        void writeClassElements(ClassScope classScope) {
          for (ElementData data : key2ElementData.values()) {
            data.generateCode(classScope);
          }
          key2ElementData.clear();
        }
      }
    }

    private static class MethodScopeImpl extends ClassScopeImpl implements MethodScope {
      private final State state;

      public MethodScopeImpl(ClassScopeImpl classScopeImpl) {
        super(classScopeImpl);
        state = new State();
      }

      @Override
      public String newMethodScopedName(String prefix) {
        return state.newMethodScopedName(prefix);
      }

      private static class State {
        private int nextId = 0;

        String newMethodScopedName(String prefix) {
          return prefix + nextId++;
        }
      }
    }
  }
}
