/*
 * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.codegen.builder.impl;

import java.lang.annotation.Annotation;
import java.util.List;

import org.jboss.errai.codegen.BlockStatement;
import org.jboss.errai.codegen.Context;
import org.jboss.errai.codegen.DefModifiers;
import org.jboss.errai.codegen.DefParameters;
import org.jboss.errai.codegen.InnerClass;
import org.jboss.errai.codegen.Parameter;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.ThrowsDeclaration;
import org.jboss.errai.codegen.builder.*;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaClass;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaConstructor;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaField;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaMethod;
import org.jboss.errai.codegen.util.GenUtil;

/**
 * @author Mike Brock <cbrock@redhat.com>
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public class ClassBuilder<T extends ClassStructureBuilder<T>> implements
        ClassDefinitionBuilderCommentOption<T>,
        ClassDefinitionBuilderScope<T>,
        ClassDefinitionStaticOption<T>,
        ClassStructureBuilder<T> {

  protected final BuildMetaClass classDefinition;

  ClassBuilder(final String className, final MetaClass parent, final Context context) {
    this.classDefinition = new BuildMetaClass(context, className);
    this.classDefinition.setSuperClass(parent);
    context.attachClass(classDefinition);
  }

  ClassBuilder(final ClassBuilder<T> that, final Context context) {
    this.classDefinition = that.classDefinition;
    this.classDefinition.setContext(context);
    context.attachClass(classDefinition);
  }

  public static ClassDefinitionBuilderCommentOption<?> define(final String fullyQualifiedName) {
    return new ClassBuilder<DefaultClassStructureBuilder>(fullyQualifiedName, null, Context.create().autoImport());
  }

  public static ClassDefinitionBuilderCommentOption<?> define(final String fullQualifiedName, final MetaClass parent) {
    return new ClassBuilder<DefaultClassStructureBuilder>(fullQualifiedName, parent, Context.create().autoImport());
  }

  public static ClassDefinitionBuilderCommentOption<?> define(final String fullQualifiedName, final Class<?> parent) {
    return define(fullQualifiedName, MetaClassFactory.get(parent));
  }

  public static ClassStructureBuilder<?> implement(final MetaClass cls) {
    return new ClassBuilder<DefaultClassStructureBuilder>(cls.getFullyQualifiedName() + "Impl", null, Context.create()
            .autoImport())
            .publicScope()
            .implementsInterface(cls).body();
  }

  public static ClassStructureBuilder<?> implement(final Class<?> cls) {
    return implement(MetaClassFactory.get(cls));
  }

  @Override
  public ClassBuilderAbstractMethodOption abstractClass() {
    classDefinition.setAbstract(true);
    return new ClassBuilderAbstractMethodOption(this, classDefinition.getContext());
  }

  @Override
  public ClassDefinitionBuilderInterfaces<ClassStructureBuilderAbstractMethodOption> interfaceDefinition() {
    classDefinition.setInterface(true);
    return new ClassBuilderAbstractMethodOption(this, classDefinition.getContext());

  }

  public ClassBuilder<T> importsClass(final Class<?> clazz) {
    return importsClass(MetaClassFactory.get(clazz));
  }

  public ClassBuilder<T> importsClass(final MetaClass clazz) {
    classDefinition.getContext().addImport(clazz);
    return this;
  }

  @Override
  public ClassDefinitionBuilderInterfaces<T> implementsInterface(final Class<?> clazz) {
    return implementsInterface(MetaClassFactory.get(clazz));
  }

  @Override
  public ClassDefinitionBuilderInterfaces<T> implementsInterface(final MetaClass clazz) {
    if (!clazz.isInterface()) {
      throw new RuntimeException("not an interface: " + clazz.getFullyQualifiedName());
    }

    classDefinition.addInterface(clazz);
    return this;
  }

  @Override
  public ClassStructureBuilder<T> body() {
    return this;
  }

  @Override
  public ClassDefinitionBuilderScope<T> classComment(String comment) {
    classDefinition.setClassComment(comment);
    return this;
  }

  @Override
  public ClassDefinitionStaticOption<T> publicScope() {
    classDefinition.setScope(Scope.Public);
    return this;
  }

  @Override
  public ClassDefinitionStaticOption<T> privateScope() {
    classDefinition.setScope(Scope.Private);
    return this;
  }

  @Override
  public ClassDefinitionStaticOption<T> protectedScope() {
    classDefinition.setScope(Scope.Protected);
    return this;
  }

  @Override
  public ClassDefinitionStaticOption<T> packageScope() {
    classDefinition.setScope(Scope.Package);
    return this;
  }

  @Override
  public ClassDefinitionBuilderAbstractOption<T> staticClass() {
    classDefinition.setStatic(true);
    return this;
  }

  @Override
  public ConstructorBlockBuilder<T> publicConstructor() {
    return genConstructor(Scope.Public, DefParameters.none());
  }

  @Override
  public ConstructorBlockBuilder<T> publicConstructor(final MetaClass... parms) {
    return genConstructor(Scope.Public, DefParameters.fromTypeArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> publicConstructor(final Class<?>... parms) {
    return publicConstructor(MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> publicConstructor(final Parameter... parms) {
    return genConstructor(Scope.Public, DefParameters.fromParameters(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> privateConstructor() {
    return genConstructor(Scope.Private, DefParameters.none());
  }

  @Override
  public ConstructorBlockBuilder<T> privateConstructor(final MetaClass... parms) {
    return genConstructor(Scope.Private, DefParameters.fromTypeArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> privateConstructor(final Class<?>... parms) {
    return privateConstructor(MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> privateConstructor(final Parameter... parms) {
    return genConstructor(Scope.Private, DefParameters.fromParameters(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> protectedConstructor() {
    return genConstructor(Scope.Protected, DefParameters.none());
  }

  @Override
  public ConstructorBlockBuilder<T> protectedConstructor(final MetaClass... parms) {
    return genConstructor(Scope.Protected, DefParameters.fromTypeArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> protectedConstructor(final Class<?>... parms) {
    return protectedConstructor(MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> protectedConstructor(final Parameter... parms) {
    return genConstructor(Scope.Protected, DefParameters.fromParameters(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> packageConstructor() {
    return genConstructor(Scope.Package, DefParameters.none());
  }

  @Override
  public ConstructorBlockBuilder<T> packageConstructor(final MetaClass... parms) {
    return genConstructor(Scope.Package, DefParameters.fromTypeArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> packageConstructor(final Class<?>... parms) {
    return packageConstructor(MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public ConstructorBlockBuilder<T> packageConstructor(final Parameter... parms) {
    return genConstructor(Scope.Package, DefParameters.fromParameters(parms));
  }

  private ConstructorBlockBuilder<T> genConstructor(final Scope scope, final DefParameters
          defParameters) {
    return new ConstructorBlockBuilderImpl<T>(new BuildCallback<T>() {

      @Override
      public T callback(final Statement statement) {
        final BuildMetaConstructor buildMetaConstructor =
                new BuildMetaConstructor(classDefinition, statement, defParameters);
        buildMetaConstructor.setScope(scope);

        classDefinition.addConstructor(buildMetaConstructor);
        return (T) ClassBuilder.this;
      }

      @Override
      public Context getParentContext() {
        return classDefinition.getContext();
      }
    });
  }

  // public method //
  @Override
  public MethodCommentBuilder<T> publicMethod(final MetaClass returnType, final String name) {
    return genMethod(Scope.Public, returnType, name, DefParameters.none());
  }

  @Override
  public MethodCommentBuilder<T> publicMethod(final Class<?> returnType, final String name) {
    return publicMethod(MetaClassFactory.get(returnType), name);
  }

  @Override
  public MethodCommentBuilder<T> publicMethod(final MetaClass returnType, final String name, final MetaClass... parms) {
    return genMethod(Scope.Public, returnType, name, DefParameters.fromTypeArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> publicMethod(final Class<?> returnType, final String name, final Class<?>... parms) {
    return publicMethod(MetaClassFactory.get(returnType), name, MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> publicMethod(final MetaClass returnType, final String name, final Parameter... parms) {
    return genMethod(Scope.Public, returnType, name, DefParameters.fromParameters(parms));
  }

  @Override
  public MethodCommentBuilder<T> publicMethod(final Class<?> returnType, final String name, final Parameter... parms) {
    return publicMethod(MetaClassFactory.get(returnType), name, parms);
  }

  // private method //
  @Override
  public MethodCommentBuilder<T> privateMethod(final MetaClass returnType, final String name) {
    return genMethod(Scope.Private, returnType, name, DefParameters.none());
  }

  @Override
  public MethodCommentBuilder<T> privateMethod(final Class<?> returnType, final String name) {
    return privateMethod(MetaClassFactory.get(returnType), name);
  }

  @Override
  public MethodCommentBuilder<T> privateMethod(final MetaClass returnType, final String name, final MetaClass... parms) {
    return genMethod(Scope.Private, returnType, name, DefParameters.fromTypeArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> privateMethod(final Class<?> returnType, final String name, final Class<?>... parms) {
    return privateMethod(MetaClassFactory.get(returnType), name, MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> privateMethod(final MetaClass returnType, final String name, final Parameter... parms) {
    return genMethod(Scope.Private, returnType, name, DefParameters.fromParameters(parms));
  }

  @Override
  public MethodCommentBuilder<T> privateMethod(final Class<?> returnType, final String name, final Parameter... parms) {
    return privateMethod(MetaClassFactory.get(returnType), name, parms);
  }

  // protected method //
  @Override
  public MethodCommentBuilder<T> protectedMethod(final MetaClass returnType, final String name) {
    return genMethod(Scope.Protected, returnType, name, DefParameters.none());
  }

  @Override
  public MethodCommentBuilder<T> protectedMethod(final Class<?> returnType, final String name) {
    return protectedMethod(MetaClassFactory.get(returnType), name);
  }

  @Override
  public MethodCommentBuilder<T> protectedMethod(final MetaClass returnType, final String name, final MetaClass... parms) {
    return genMethod(Scope.Protected, returnType, name, DefParameters.fromTypeArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> protectedMethod(final Class<?> returnType, final String name, final Class<?>... parms) {
    return protectedMethod(MetaClassFactory.get(returnType), name, MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> protectedMethod(final MetaClass returnType, final String name, final Parameter... parms) {
    return genMethod(Scope.Protected, returnType, name, DefParameters.fromParameters(parms));
  }

  @Override
  public MethodCommentBuilder<T> protectedMethod(final Class<?> returnType, final String name, final Parameter... parms) {
    return protectedMethod(MetaClassFactory.get(returnType), name, parms);
  }

  // package-private method //
  @Override
  public MethodCommentBuilder<T> packageMethod(final MetaClass returnType, final String name) {
    return genMethod(Scope.Package, returnType, name, DefParameters.none());
  }

  @Override
  public MethodCommentBuilder<T> packageMethod(final Class<?> returnType, final String name) {
    return packageMethod(MetaClassFactory.get(returnType), name);
  }

  @Override
  public MethodCommentBuilder<T> packageMethod(final MetaClass returnType, final String name, final MetaClass... parms) {
    return genMethod(Scope.Package, returnType, name, DefParameters.fromTypeArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> packageMethod(final Class<?> returnType, final String name, final Class<?>... parms) {
    return packageMethod(MetaClassFactory.get(returnType), name, MetaClassFactory.fromClassArray(parms));
  }

  @Override
  public MethodCommentBuilder<T> packageMethod(final MetaClass returnType, final String name, final Parameter... parms) {
    return genMethod(Scope.Package, returnType, name, DefParameters.fromParameters(parms));
  }

  @Override
  public MethodCommentBuilder<T> packageMethod(final Class<?> returnType, final String name, final Parameter... parms) {
    return packageMethod(MetaClassFactory.get(returnType), name, parms);
  }

  private MethodCommentBuilder<T> genMethod(final Scope scope,
                                            final MetaClass returnType,
                                            final String name,
                                            final DefParameters defParameters) {

    return new MethodBlockBuilderImpl<T>(new MethodBuildCallback<T>() {
      @Override
      public T callback(final BlockStatement statement,
                        final DefParameters parameters,
                        final DefModifiers modifiers,
                        final ThrowsDeclaration throwsDeclaration,
                        final List<Annotation> annotations,
                        final String comment) {

        final DefParameters dParameters;

        if (parameters != null) {
          dParameters = parameters;
        }
        else {
          dParameters = defParameters;
        }

        final BuildMetaMethod buildMetaMethod = new BuildMetaMethod(classDefinition, statement, scope,
                modifiers, name, returnType, dParameters, throwsDeclaration);

        if (annotations != null) {
          buildMetaMethod.addAnnotations(annotations);
        }

        buildMetaMethod.setMethodComment(comment);
        classDefinition.addMethod(buildMetaMethod);
        return (T) ClassBuilder.this;
      }
    });
  }

  @Override
  public FieldBuildStart<T> publicField(final String name, final MetaClass type) {
    return genField(Scope.Public, name, type);
  }

  @Override
  public FieldBuildStart<T> publicField(final String name, final Class<?> type) {
    return publicField(name, MetaClassFactory.get(type));
  }

  @Override
  public FieldBuildStart<T> privateField(final String name, final MetaClass type) {
    return genField(Scope.Private, name, type);
  }

  @Override
  public FieldBuildStart<T> privateField(final String name, final Class<?> type) {
    return privateField(name, MetaClassFactory.get(type));
  }

  @Override
  public FieldBuildStart<T> protectedField(final String name, final MetaClass type) {
    return genField(Scope.Protected, name, type);
  }

  @Override
  public FieldBuildStart<T> protectedField(final String name, final Class<?> type) {
    return protectedField(name, MetaClassFactory.get(type));
  }

  @Override
  public FieldBuildStart<T> packageField(final String name, final MetaClass type) {
    return genField(Scope.Package, name, type);
  }

  @Override
  public FieldBuildStart<T> packageField(final String name, final Class<?> type) {
    return packageField(name, MetaClassFactory.get(type));
  }

  private FieldBuildStart<T> genField(final Scope scope, final String name,
                                      final MetaClass type) {
    return new FieldBuilder<T>(new BuildCallback<T>() {
      @Override
      public T callback(final Statement statement) {
        final BuildMetaField buildMetaField
                = new BuildMetaField(classDefinition, statement, scope, type, name);

        classDefinition.addField(buildMetaField);
        return (T) ClassBuilder.this;
      }

      @Override
      public Context getParentContext() {
        return classDefinition.getContext();
      }
    }, scope, type, name);
  }

  @Override
  public BuildMetaClass getClassDefinition() {
    return classDefinition;
  }

  @Override
  public String toJavaString() {
    try {
      return classDefinition.toJavaString();
    }
    catch (Throwable t) {
      GenUtil.throwIfUnhandled("error generating class: " + classDefinition.getFullyQualifiedName(), t);
      return null;
    }
  }

  @Override
  public ClassStructureBuilder<T> declaresInnerClass(InnerClass ic) {
    classDefinition.addInnerClass(ic);
    return this;
  }
}
