/*
 * Copyright 2007 Tim Wood
 *
 * 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 uk.co.flamingpenguin.jewel.cli;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

import com.lexicalscope.fluentreflection.ReflectedClass;
import com.lexicalscope.fluentreflection.ReflectedMethod;

class OptionSpecificationParser {
    private static final Logger logger = Logger.getLogger(OptionSpecificationParser.class.getName());

    private final Method method;
    private final Class<?> klass;

    public OptionSpecificationParser(final ReflectedClass<?> klass, final ReflectedMethod method) {
        this.klass = klass.classUnderReflection();
        this.method = method.methodUnderReflection();
    }

    OptionSpecificationImpl buildOptionSpecification(final OptionsSpecificationBuilder builder) {
        final OptionSpecificationBuilder optionSpecificationBuilder = new OptionSpecificationBuilder(method);

        final Type returnType = method.getGenericReturnType();
        final Class<?> type = (Class<?>) (isList(method.getReturnType()) ? getListType(returnType) : returnType);
        optionSpecificationBuilder.setType(type);

        final boolean multiValued = isList(method.getReturnType());
        optionSpecificationBuilder.setMultiValued(multiValued);

        final String baseName = extractBaseMethodName(method);
        final Method optionalityMethod = findCorrespondingOptionalityMethod(baseName, klass);
        if (optionalityMethod != null) {
            optionSpecificationBuilder.setOptionalityMethod(optionalityMethod);
        }

        if (method.isAnnotationPresent(Option.class)) {
            final Option optionAnnotation = method.getAnnotation(Option.class);

            final String[] shortNameSpecification = optionAnnotation.shortName();
            final List<String> shortNames = new ArrayList<String>();
            for (final String element : shortNameSpecification) {
                final String shortName = element.trim();
                if (shortName.length() > 0) {
                    shortNames.add(element.substring(0, 1));
                }
            }
            optionSpecificationBuilder.setShortNames(shortNames);

            final String longNameSpecification = optionAnnotation.longName().trim();
            final String longName = nullOrBlank(longNameSpecification) ? baseName : longNameSpecification;
            optionSpecificationBuilder.setLongName(longName);

            final String description = optionAnnotation.description().trim();
            optionSpecificationBuilder.setDescription(description);

            final String pattern = optionAnnotation.pattern();
            optionSpecificationBuilder.setPattern(pattern);

            final List<String> defaultValue = Arrays.asList(optionAnnotation.defaultValue());
            optionSpecificationBuilder.setDefaultValue(defaultValue);

            final boolean helpRequest = optionAnnotation.helpRequest();
            optionSpecificationBuilder.setHelpRequest(helpRequest);

            builder.addOption(optionSpecificationBuilder.createOptionSpecification());
        } else if (method.isAnnotationPresent(Unparsed.class)) {
            final Unparsed annotation = method.getAnnotation(Unparsed.class);

            optionSpecificationBuilder.setLongName(annotation.name());
            optionSpecificationBuilder.setDescription("");
            optionSpecificationBuilder.setPattern(".*");
            optionSpecificationBuilder.setDefaultValue(Collections.<String>emptyList());
            optionSpecificationBuilder.setHelpRequest(false);

            builder.addUnparsedOption(optionSpecificationBuilder.createOptionSpecification());
        }

        return optionSpecificationBuilder.createOptionSpecification();
    }

    private boolean nullOrBlank(final String string) {
        return string == null || string.equals("");
    }

    private final boolean isList(final Class<?> klass) {
        return klass.isAssignableFrom(List.class);
    }

    private final Class<?> getListType(final Type genericReturnType) {
        if (genericReturnType instanceof ParameterizedType) {
            return (Class<?>) ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
        } else {
            logger.finer("Found raw List type; assuming List<String>.");
            return String.class;
        }
    }

    private final Method findCorrespondingOptionalityMethod(final String name, final Class<?> klass) {
        try {
            final Method method = klass.getMethod(addPrefix("is", name), new Class[] {});
            if (isBoolean(method.getReturnType())) {
                return method;
            }
            return null;
        } catch (final NoSuchMethodException e) {
            return null;
        }
    }

    private String addPrefix(final String prefix, final String name) {
        return prefix + name.substring(0, 1).toUpperCase() + name.substring(1);
    }

    private String extractBaseMethodName(final Method method) {
        final String methodName = method.getName();

        final String isPrefix = "is";
        if (isBoolean(method.getReturnType()) && methodName.startsWith(isPrefix)) {
            return stripPrefix(methodName, isPrefix);
        }
        return stripPrefix(methodName, "get");
    }

    private final boolean isBoolean(final Class<?> type) {
        return type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class);
    }

    private final String stripPrefix(final String methodName, final String prefix) {
        if (methodName.length() > prefix.length() && methodName.startsWith(prefix)) {
            return methodName.substring(prefix.length(), prefix.length() + 1).toLowerCase()
                    + (methodName.length() > prefix.length() + 1 ? methodName.substring(prefix.length() + 1) : "");
        }
        return methodName;
    }
}

@@ -616,16 +616,87 @@
 l.cli;%0A%0A
+import static com.lexicalscope.fluentreflection.ReflectionMatchers.*;%0A%0A
 import j
@@ -1231,32 +1231,41 @@
   private final 
+Reflected
 Class%3C?%3E klass;%0A
@@ -1395,31 +1395,8 @@
 lass
-.classUnderReflection()
 ;%0A  
@@ -2097,24 +2097,33 @@
       final 
+Reflected
 Method optio
@@ -2304,24 +2304,48 @@
 nalityMethod
+.methodUnderReflection()
 );%0A        %7D
@@ -5164,24 +5164,33 @@
 ivate final 
+Reflected
 Method findC
@@ -5240,24 +5240,33 @@
 name, final 
+Reflected
 Class%3C?%3E kla
@@ -5283,161 +5283,195 @@
     
-try %7B%0A            final 
+final List%3CReflected
 Method
+%3E
  method
+s
  =
+%0A               
  klass.
-getM
+m
 ethod
-(addPrefix(%22is%22, name), new Class%5B%5D %7B%7D);%0A            if (isBoolean(method.getReturnType()
+s(%0A                        callableHasName(addPrefix(%22is%22, name)).and(isExistence()));%0A        if (!methods.isEmpty(
 )) %7B%0A
-    
     
@@ -5495,103 +5495,28 @@
 thod
-;%0A            %7D%0A            return null;%0A        %7D catch (final NoSuchMethodException e) %7B%0A    
+s.get(0);%0A        %7D%0A
     
@@ -5532,26 +5532,16 @@
 n null;%0A
-        %7D%0A
     %7D%0A%0A 

use fluent-reflection to simplify implementation