/*
 * 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.tomcat.util.xreflection;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Set;
import java.util.TreeSet;

import org.apache.tomcat.util.IntrospectionUtils;

public final class SetPropertyClass implements Comparable<SetPropertyClass> {

    static final String OBJECT_VAR_NAME = "o";
    static final String NAME_VAR_NAME = "name";
    static final String VALUE_VAR_NAME = "value";
    static final String SETP_VAR_NAME = "invokeSetProperty";

    private final SetPropertyClass parent;
    private final Class<?> clazz;
    private final Set<SetPropertyClass> children = new TreeSet<>();
    private final Set<ReflectionProperty> properties = new TreeSet<>();
    private final boolean isAbstract;
    private final Method genericSetPropertyMethod;
    private final Method genericGetPropertyMethod;

    SetPropertyClass(Class<?> clazz, SetPropertyClass parent) {
        this.clazz = clazz;
        this.parent = parent;
        this.isAbstract = Modifier.isAbstract(clazz.getModifiers());
        Method classSetter, classGetter;
        try {
            classSetter = clazz.getDeclaredMethod("setProperty", String.class, String.class);
        } catch (NoSuchMethodException e) {
            try {
                classSetter = clazz.getDeclaredMethod("setProperty", String.class, Object.class);
            } catch (NoSuchMethodException x) {
                classSetter = null;
            }
        }
        try {
            classGetter = clazz.getDeclaredMethod("getProperty", String.class);
        } catch (NoSuchMethodException e) {
            classGetter = null;
        }
        genericSetPropertyMethod = classSetter;
        genericGetPropertyMethod = classGetter;
    }

    boolean isAbstract() {
        return isAbstract;
    }

    void addSubClass(SetPropertyClass clazz) {
        this.children.add(clazz);
    }

    boolean isBaseClass() {
        return parent == null;
    }

    public Set<SetPropertyClass> getChildren() {
        return children;
    }

    public Set<ReflectionProperty> getProperties() {
        return properties;
    }

    public Method getGenericSetPropertyMethod() {
        return genericSetPropertyMethod;
    }

    public Method getGenericGetPropertyMethod() {
        return genericGetPropertyMethod;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        SetPropertyClass that = (SetPropertyClass) o;

        return clazz.equals(that.clazz);
    }

    @Override
    public int hashCode() {
        return clazz.hashCode();
    }

    public SetPropertyClass getParent() {
        return parent;
    }

    public Class<?> getClazz() {
        return clazz;
    }

    @Override
    public String toString() {
        return "SetPropertyClass{" + "clazz=" + clazz.getName() + '}';
    }

    public void addProperty(ReflectionProperty property) {
        properties.add(property);
    }


    public String generateSetPropertyMethod(ReflectionProperty property) {
        // this property has a setProperty method
        if (property.hasSetPropertySetter()) {
            return "((" + this.getClazz().getName().replace('$', '.') + ")" + OBJECT_VAR_NAME + ")." +
                    property.getSetMethod().getName() + "(" + NAME_VAR_NAME + ", " + VALUE_VAR_NAME + ");";
        }

        // direct setter
        if (property.hasSetter()) {
            return "((" + this.getClazz().getName().replace('$', '.') + ")" + OBJECT_VAR_NAME + ")." +
                    property.getSetMethod().getName() + "(" + property.getConversion(VALUE_VAR_NAME) + ");";
        }
        return null;
    }

    public String generateGetPropertyMethod(ReflectionProperty property) {
        // this property has a getProperty method
        if (property.hasGetPropertyGetter()) {
            return "result = ((" + this.getClazz().getName().replace('$', '.') + ")" + OBJECT_VAR_NAME + ")." +
                    property.getGetMethod().getName() + "(" + NAME_VAR_NAME + ");";
        }

        // direct getter
        if (property.hasGetter()) {
            return "result = ((" + this.getClazz().getName().replace('$', '.') + ")" + OBJECT_VAR_NAME + ")." +
                    property.getGetMethod().getName() + "();";
        }
        return null;
    }

    public String generateSetPropertyForMethod() {
        //@formatter:off
        StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(1))
            .append(generatesSetPropertyForMethodHeader())
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(2))
            .append("switch (")
            .append(NAME_VAR_NAME)
            .append(") {")
            .append(System.lineSeparator());

        // case statements for each property
        for (ReflectionProperty property : getProperties()) {
            String invocation = generateSetPropertyMethod(property);
            if (invocation != null) {
                code.append(ReflectionLessCodeGenerator.getIndent(3))
                    .append("case \"")
                    .append(property.getPropertyName())
                    .append("\" : ")
                    .append(System.lineSeparator());

                code.append(ReflectionLessCodeGenerator.getIndent(4))
                    .append(invocation)
                    .append(System.lineSeparator())
                    .append(ReflectionLessCodeGenerator.getIndent(4))
                    .append("return true;")
                    .append(System.lineSeparator());

            } else {
                code.append(ReflectionLessCodeGenerator.getIndent(3)).append("//no set")
                    .append(IntrospectionUtils.capitalize(
                            property.getPropertyName())).append(" method found on this class")
                    .append(System.lineSeparator());
            }
        }

        // end switch statement
        code.append(ReflectionLessCodeGenerator.getIndent(2))
            .append('}')
            .append(System.lineSeparator());

        // we have a generic setProperty(String, String) method, invoke it
        if (getGenericSetPropertyMethod() != null) {
            ReflectionProperty p = new ReflectionProperty(
                clazz.getName(),
                "property",
                String.class,
                getGenericSetPropertyMethod(),
                null
            );
           code.append(ReflectionLessCodeGenerator.getIndent(2))
               .append("if (")
               .append(SETP_VAR_NAME)
               .append(") {")
               .append(System.lineSeparator())
               .append(ReflectionLessCodeGenerator.getIndent(3))
               .append(generateSetPropertyMethod(p))
               .append(System.lineSeparator())
               .append(ReflectionLessCodeGenerator.getIndent(3))
               .append("return true;")
               .append(System.lineSeparator())
               .append(ReflectionLessCodeGenerator.getIndent(2))
               .append('}')
               .append(System.lineSeparator());
        }

        // invoke parent or return false
        code.append(ReflectionLessCodeGenerator.getIndent(2))
            .append("return ")
            .append(getSetPropertyForExitStatement())
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(1))
            .append('}');

        return code.toString();
        //@formatter:on
    }

    private String getSetPropertyForExitStatement() {

        return (getParent() != null) ?
                // invoke the parent if we have one
                getParent().generateParentSetPropertyForMethodInvocation() :
                // if we invoke setProperty, return true, return false otherwise
                getGenericSetPropertyMethod() != null ? "true;" : "false;";
    }

    public String generateInvocationSetForPropertyCaseStatement(int level) {
        //@formatter:off
        StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(level))
            .append("case \"")
            .append(getClazz().getName())
            .append("\" : ")
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(level+1))
            .append("return ")
            .append(generateParentSetPropertyForMethodInvocation())
            .append(System.lineSeparator());
        return code.toString();
        //@formatter:on
    }

    public String generateParentSetPropertyForMethodInvocation() {
        String[] classParts = clazz.getName().split("\\.|\\$");
        StringBuilder methodInvocation = new StringBuilder("setPropertyFor");
        for (String s : classParts) {
            methodInvocation.append(IntrospectionUtils.capitalize(s));
        }
        //@formatter:off
        methodInvocation.append('(')
            .append(OBJECT_VAR_NAME)
            .append(", ")
            .append(NAME_VAR_NAME)
            .append(", ")
            .append(VALUE_VAR_NAME)
            .append(", ")
            .append(SETP_VAR_NAME)
            .append(");");
        return methodInvocation.toString();
        //@formatter:on
    }

    public String generatesSetPropertyForMethodHeader() {
        String[] classParts = clazz.getName().split("\\.|\\$");
        StringBuilder methodInvocation = new StringBuilder("private static boolean setPropertyFor");
        for (String s : classParts) {
            methodInvocation.append(IntrospectionUtils.capitalize(s));
        }
        //@formatter:off
        methodInvocation.append("(Object ")
            .append(OBJECT_VAR_NAME)
            .append(", String ")
            .append(NAME_VAR_NAME)
            .append(", String ")
            .append(VALUE_VAR_NAME)
            .append(", boolean ")
            .append(SETP_VAR_NAME)
            .append(") {");
        return methodInvocation.toString();
        //@formatter:on
    }

    public String generateInvocationGetForPropertyCaseStatement(int level) {
        //@formatter:off
        StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(level))
            .append("case \"")
            .append(getClazz().getName())
            .append("\" : ")
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(level+1))
            .append("result = ")
            .append(generateParentGetPropertyForMethodInvocation())
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(level+1))
            .append("break;")
            .append(System.lineSeparator());
        return code.toString();
        //@formatter:on
    }

    public String generateParentGetPropertyForMethodInvocation() {
        String[] classParts = clazz.getName().split("\\.|\\$");
        StringBuilder methodInvocation = new StringBuilder("getPropertyFor");
        for (String s : classParts) {
            methodInvocation.append(IntrospectionUtils.capitalize(s));
        }
        //@formatter:off
        methodInvocation.append('(')
            .append(OBJECT_VAR_NAME)
            .append(", ")
            .append(NAME_VAR_NAME)
            .append(");");
        return methodInvocation.toString();
        //@formatter:on
    }

    public String generatesGetPropertyForMethodHeader() {
        String[] classParts = clazz.getName().split("\\.|\\$");
        StringBuilder methodInvocation = new StringBuilder("private static Object getPropertyFor");
        for (String s : classParts) {
            methodInvocation.append(IntrospectionUtils.capitalize(s));
        }
        //@formatter:off
        methodInvocation.append("(Object ")
            .append(OBJECT_VAR_NAME)
            .append(", String ")
            .append(NAME_VAR_NAME)
            .append(") {");
        return methodInvocation.toString();
        //@formatter:on
    }

    private String getGetPropertyForExitStatement() {
        if (getParent() != null) {
            return getParent().generateParentGetPropertyForMethodInvocation();
        }
        return "null;";
    }


    public String generateGetPropertyForMethod() {
        //@formatter:off
        StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(1))
            .append(generatesGetPropertyForMethodHeader())
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(2))
            .append("Object result = null;")
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(2))
            .append("switch (")
            .append(NAME_VAR_NAME)
            .append(") {")
            .append(System.lineSeparator());

        // case statements for each property
        for (ReflectionProperty property : getProperties()) {
            String invocation = generateGetPropertyMethod(property);
            if (invocation != null) {
                code.append(ReflectionLessCodeGenerator.getIndent(3))
                    .append("case \"")
                    .append(property.getPropertyName())
                    .append("\" : ")
                    .append(System.lineSeparator());

                code.append(ReflectionLessCodeGenerator.getIndent(4))
                    .append(invocation)
                    .append(System.lineSeparator())
                    .append(ReflectionLessCodeGenerator.getIndent(4))
                    .append("break;")
                    .append(System.lineSeparator());
            } else {
                code.append(ReflectionLessCodeGenerator.getIndent(3)).append("//no get")
                    .append(IntrospectionUtils.capitalize(property.getPropertyName())).append(" method found on this class")
                    .append(System.lineSeparator());
            }
        }

        // end switch statement
        code.append(ReflectionLessCodeGenerator.getIndent(2))
            .append('}')
            .append(System.lineSeparator());

        // invoke parent or return null
        code.append(ReflectionLessCodeGenerator.getIndent(2))
            .append("if (result == null) {")
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(3))
            .append("result = ")
            .append(getGetPropertyForExitStatement())
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(2))
            .append('}')
            .append(System.lineSeparator());

        // we have a generic getProperty(String, String) method, invoke it
        if (getGenericGetPropertyMethod() != null) {
            ReflectionProperty p = new ReflectionProperty(
                clazz.getName(),
                "property",
                String.class,
                null,
                getGenericGetPropertyMethod()
            );
            code.append(ReflectionLessCodeGenerator.getIndent(2))
                .append("if (result == null) {")
                .append(System.lineSeparator())
                .append(ReflectionLessCodeGenerator.getIndent(3))
                .append(generateGetPropertyMethod(p))
                .append(System.lineSeparator())
                .append(ReflectionLessCodeGenerator.getIndent(2))
                .append('}')
                .append(System.lineSeparator());
        }
        code.append(ReflectionLessCodeGenerator.getIndent(2))
            .append("return result;")
            .append(System.lineSeparator())
            .append(ReflectionLessCodeGenerator.getIndent(1))
            .append('}')
            .append(System.lineSeparator());

        return code.toString();
        //@formatter:on
    }

    @Override
    public int compareTo(SetPropertyClass o) {
        return clazz.getName().compareTo(o.clazz.getName());
    }
}
