/*
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */

package org.jboss.forge.roaster.model.impl;

import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TextElement;
import org.jboss.forge.roaster.model.Annotation;
import org.jboss.forge.roaster.model.JavaType;
import org.jboss.forge.roaster.model.Method;
import org.jboss.forge.roaster.model.Type;
import org.jboss.forge.roaster.model.Visibility;
import org.jboss.forge.roaster.model.source.AnnotationSource;
import org.jboss.forge.roaster.model.source.FieldSource;
import org.jboss.forge.roaster.model.source.JavaSource;
import org.jboss.forge.roaster.model.source.MethodSource;
import org.jboss.forge.roaster.model.source.ParameterSource;
import org.jboss.forge.roaster.model.source.PropertyHolderSource;
import org.jboss.forge.roaster.model.source.PropertySource;

/**
 * Implementation of PropertySource.
 *
 * @param <O>
 * @author mbenson
 * @author <a href="ggastald@redhat.com">George Gastaldi</a>
 */
class PropertyImpl<O extends JavaSource<O> & PropertyHolderSource<O>> implements PropertySource<O>
{

   private final O origin;
   private String name;

   PropertyImpl(String name, O origin)
   {
      super();
      this.origin = origin;
      this.name = name;
   }

   @Override
   public Object getInternal()
   {
      return getOrigin().getInternal();
   }

   @Override
   public O getOrigin()
   {
      return origin;
   }

   @Override
   public String getName()
   {
      return name == null ? "<missing>" : name;
   }

   @Override
   public Type<O> getType()
   {
      if (isAccessible())
      {
         return getAccessor().getReturnType();
      }
      if (isMutable())
      {
         return getMutator().getParameters().get(0).getType();
      }
      if (hasField())
      {
         return getField().getType();
      }
      return null;
   }

   @Override
   public boolean hasField()
   {
      return getField() != null;
   }

   @Override
   public FieldSource<O> getField()
   {
      final FieldSource<O> field = getOrigin().getField(name);
      if (field != null && !field.isStatic())
      {
         return field;
      }
      return null;
   }

   @Override
   public boolean isAccessible()
   {
      return getAccessor() != null;
   }

   @Override
   public boolean isMutable()
   {
      return getMutator() != null;
   }

   @Override
   public MethodSource<O> getAccessor()
   {
      for (MethodSource<O> method : getOrigin().getMethods())
      {
         if (isAccessor(method))
         {
            return method;
         }
      }
      return null;
   }

   @Override
   public MethodSource<O> getMutator()
   {
      final Type<O> type;
      if (hasField())
      {
         type = getField().getType();
      }
      else if (isAccessible())
      {
         type = getAccessor().getReturnType();
      }
      else
      {
         type = null;
      }

      for (MethodSource<O> method : getOrigin().getMethods())
      {
         if (isMutator(method))
         {
            if (type == null
                     || Objects.equals(type.getQualifiedName(), method.getParameters().get(0).getType()
                     .getQualifiedName()))
            {
               return method;
            }
         }
      }
      return null;
   }

   @Override
   public MethodSource<O> createAccessor()
   {
      if (getAccessor() != null)
      {
         throw new IllegalStateException("Accessor method already exists");
      }

      final Type<O> type = getType();
      final String accessorName = methodName(type.isType(boolean.class) ? "is" : "get", name);
      final MethodSource<O> result = getOrigin().addMethod().setReturnType(typeName())
               .setName(accessorName);

      if (!getOrigin().isInterface())
      {
         result.setVisibility(Visibility.PUBLIC);
         if (hasField())
         {
            final String body = String.format("return %s;", getName());
            result.setBody(body);
         }
      }
      return result;
   }

   @Override
   public MethodSource<O> createMutator()
   {
      if (getMutator() != null)
      {
         throw new IllegalStateException("Mutator method already exists");
      }

      final String mutatorName = methodName("set", name);
      final String parameters = String.format("%s %s", typeName(), getName());

      final MethodSource<O> result = getOrigin().addMethod().setReturnTypeVoid().setName(mutatorName)
               .setParameters(parameters);

      if (!getOrigin().isInterface())
      {
         result.setVisibility(Visibility.PUBLIC);
         if (hasField())
         {
            final String body = String.format("this.%1$s = %1$s;", getName());
            result.setBody(body);
         }
      }
      return result;
   }

   @Override
   public FieldSource<O> createField()
   {
      if (getOrigin().isInterface())
      {
         throw new IllegalStateException("An interface cannot declare a nonstatic field");
      }
      if (getField() != null)
      {
         throw new IllegalStateException("Field already exists");
      }
      final FieldSource<O> result = getOrigin().addField().setVisibility(Visibility.PRIVATE).setType(typeName())
               .setName(name);
      if (getOrigin().isEnum())
      {
         result.setFinal(true);
      }
      if (isAccessible() && !getAccessor().isAbstract())
      {
         removeAccessor();
         createAccessor();
      }
      if (isMutable() && !getMutator().isAbstract())
      {
         removeMutator();
         createMutator();
      }
      return result;
   }

   @Override
   public PropertySource<O> setName(final String name)
   {
      if (StringUtils.isBlank(name))
      {
         throw new IllegalStateException("Property name cannot be null/empty/blank");
      }

      if (hasField())
      {
         getField().setName(name);
      }

      final String oldName = this.name;
      final boolean visitDocTags = true;

      final ASTVisitor renameVisitor = new ASTVisitor(visitDocTags)
      {
         @Override
         public boolean visit(SimpleName node)
         {
            if (Objects.equals(oldName, node.getIdentifier()))
            {
               node.setIdentifier(name);
            }
            return super.visit(node);
         }

         @Override
         public boolean visit(TextElement node)
         {
            final String text = node.getText();
            if (!text.contains(oldName))
            {
               return super.visit(node);
            }
            final int matchLength = oldName.length();
            final int textLength = text.length();
            final StringBuilder buf = new StringBuilder(text.length());
            final ParsePosition pos = new ParsePosition(0);

            while (pos.getIndex() < textLength)
            {
               final int index = pos.getIndex();
               final char c = text.charAt(index);
               if (Character.isJavaIdentifierStart(c))
               {
                  final int next = index + matchLength;

                  if (next <= textLength && Objects.equals(oldName, text.substring(index, next)))
                  {
                     buf.append(name);
                     pos.setIndex(next);
                     continue;
                  }
               }
               buf.append(c);
               pos.setIndex(index + 1);
            }

            node.setText(buf.toString());
            return super.visit(node);
         }
      };

      if (isAccessible())
      {
         final MethodSource<O> accessor = getAccessor();
         final String prefix = accessor.getReturnType().isType(boolean.class) ? "is" : "get";
         accessor.setName(methodName(prefix, name));
         ((MethodDeclaration) accessor.getInternal()).accept(renameVisitor);
      }

      if (isMutable())
      {
         final MethodSource<O> mutator = getMutator();
         mutator.setName(methodName("set", name));
         ((MethodDeclaration) mutator.getInternal()).accept(renameVisitor);
      }

      this.name = name;

      return this;
   }

   @Override
   public PropertySource<O> setType(Class<?> clazz)
   {
      return setType(clazz.getName());
   }

   @Override
   public PropertySource<O> setType(String type)
   {
      final MethodSource<O> accessor = getAccessor();
      final MethodSource<O> mutator = getMutator();
      final FieldSource<O> field = getField();

      if (accessor != null)
      {
         final Type<O> originalType = accessor.getReturnType();
         accessor.setReturnType(type);
         if (originalType.isType(boolean.class) || accessor.getReturnType().isType(boolean.class))
         {
            // potential name change:
            final String accessorName = methodName(accessor.getReturnType().isType(boolean.class) ? "is" : "get",
                     getName());
            accessor.setName(accessorName);
         }
      }
      if (mutator != null)
      {
         for (ParameterSource<O> param : mutator.getParameters())
            mutator.removeParameter(param);
         mutator.addParameter(type, getName());
      }
      if (field != null)
      {
         field.setType(type);
      }
      return this;
   }

   @Override
   public PropertySource<O> setType(JavaType<?> entity)
   {
      return setType(entity.getQualifiedName());
   }

   @Override
   public PropertySource<O> setAccessible(boolean accessible)
   {
      if (isAccessible() != accessible)
      {
         if (accessible)
         {
            createAccessor();
         }
         else
         {
            removeAccessor();
         }
      }
      return this;
   }

   @Override
   public PropertySource<O> setMutable(boolean mutable)
   {
      if (isMutable() != mutable)
      {
         if (mutable)
         {
            if (hasField())
            {
               getField().setFinal(false);
            }
            createMutator();
         }
         else
         {
            if (hasField())
            {
               getField().setFinal(true);
            }
            removeMutator();
         }
      }
      return this;
   }

   @Override
   public PropertySource<O> removeAccessor()
   {
      if (isAccessible())
      {
         getOrigin().removeMethod(getAccessor());
      }
      return this;
   }

   @Override
   public PropertySource<O> removeMutator()
   {
      if (isMutable())
      {
         getOrigin().removeMethod(getMutator());
      }
      return this;
   }

   @Override
   public PropertySource<O> removeField()
   {
      if (hasField())
      {
         getOrigin().removeField(getField());
      }
      return this;
   }

   @Override
   public String toString()
   {
      return "Property: " + getName();
   }

   @Override
   public boolean equals(Object obj)
   {
      if (obj == this)
      {
         return true;
      }
      if (!(obj instanceof PropertyImpl<?>))
      {
         return false;
      }
      final PropertyImpl<?> other = (PropertyImpl<?>) obj;
      return getOrigin() == other.getOrigin() && Objects.equals(getName(), other.getName());
   }

   @Override
   public int hashCode()
   {
      // compatible with Java 6:
      return Arrays.hashCode(new Object[] { getOrigin(), getName() });
   }

   /**
    * Helpful method to determine whether this object actually represents a real property.
    */
   public boolean isValid()
   {
      return hasField() || isAccessible() || isMutable();
   }

   private String typeName()
   {
      final Type<O> type = getType();
      return type == null ? "<missing>" : type.toString();
   }

   private boolean isAccessor(Method<O, ?> method)
   {
      if (method.isConstructor())
      {
         return false;
      }
      if (method.isReturnTypeVoid())
      {
         return false;
      }
      if (method.getParameters().isEmpty())
      {
         if (method.getReturnType().isType(boolean.class) && Objects.equals(method.getName(), methodName("is", name)))
         {
            return true;
         }
         return Objects.equals(method.getName(), methodName("get", name));
      }
      return false;
   }

   private boolean isMutator(Method<O, ?> method)
   {
      if (method.isConstructor())
      {
         return false;
      }
      return method.isReturnTypeVoid() && method.getParameters().size() == 1
               && Objects.equals(method.getName(), methodName("set", name));
   }

   private static String methodName(String prefix, String property)
   {
      return prefix + StringUtils.capitalize(property);
   }

   @Override
   public Annotation<O> getAnnotation(Class<? extends java.lang.annotation.Annotation> type)
   {
      Annotation<O> ann = null;
      FieldSource<O> field = getField();
      if (field != null)
      {
         ann = field.getAnnotation(type);
      }
      if (ann == null)
      {
         MethodSource<O> accessor = getAccessor();
         if (accessor != null)
         {
            ann = accessor.getAnnotation(type);
         }
      }
      if (ann == null)
      {
         MethodSource<O> mutator = getMutator();
         if (mutator != null)
         {
            ann = mutator.getAnnotation(type);
         }
      }
      return ann;
   }

   @Override
   public Annotation<O> getAnnotation(String type)
   {
      Annotation<O> ann = null;
      FieldSource<O> field = getField();
      if (field != null)
      {
         ann = field.getAnnotation(type);
      }
      if (ann == null)
      {
         MethodSource<O> accessor = getAccessor();
         if (accessor != null)
         {
            ann = accessor.getAnnotation(type);
         }
      }
      if (ann == null)
      {
         MethodSource<O> mutator = getMutator();
         if (mutator != null)
         {
            ann = mutator.getAnnotation(type);
         }
      }
      return ann;
   }

   @Override
   public List<? extends Annotation<O>> getAnnotations()
   {
      List<Annotation<O>> annotations = new ArrayList<>();
      FieldSource<O> field = getField();
      if (field != null)
      {
         List<AnnotationSource<O>> fieldAnnotations = field.getAnnotations();
         annotations.addAll(fieldAnnotations);
      }
      MethodSource<O> accessor = getAccessor();
      if (accessor != null)
      {
         List<AnnotationSource<O>> accessorAnnotations = accessor.getAnnotations();
         annotations.addAll(accessorAnnotations);
      }
      MethodSource<O> mutator = getMutator();
      if (mutator != null)
      {
         List<AnnotationSource<O>> mutatorAnnotations = mutator.getAnnotations();
         annotations.addAll(mutatorAnnotations);
      }
      return annotations;
   }

   @Override
   public boolean hasAnnotation(Class<? extends java.lang.annotation.Annotation> type)
   {
      boolean hasAnnotation = false;
      FieldSource<O> field = getField();
      if (field != null)
      {
         hasAnnotation = field.hasAnnotation(type);
      }
      if (!hasAnnotation)
      {
         MethodSource<O> accessor = getAccessor();
         if (accessor != null)
         {
            hasAnnotation = accessor.hasAnnotation(type);
         }
      }
      if (!hasAnnotation)
      {
         MethodSource<O> mutator = getMutator();
         if (mutator != null)
         {
            hasAnnotation = mutator.hasAnnotation(type);
         }
      }
      return hasAnnotation;
   }

   @Override
   public boolean hasAnnotation(String type)
   {
      boolean hasAnnotation = false;
      FieldSource<O> field = getField();
      if (field != null)
      {
         hasAnnotation = field.hasAnnotation(type);
      }
      if (!hasAnnotation)
      {
         MethodSource<O> accessor = getAccessor();
         if (accessor != null)
         {
            hasAnnotation = accessor.hasAnnotation(type);
         }
      }
      if (!hasAnnotation)
      {
         MethodSource<O> mutator = getMutator();
         if (mutator != null)
         {
            hasAnnotation = mutator.hasAnnotation(type);
         }
      }
      return hasAnnotation;
   }
}
