/*
 *  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.isis.applib.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.isis.applib.util.ObjectContracts.ObjectContract;
import org.apache.isis.commons.internal._Constants;
import org.apache.isis.commons.internal.base._Strings;

/**
 * Package private parser for ObjectContract.<br/><br/>
 *
 * Not public API! Use ObjectContracts.parse(String) instead.
 *
 *
 * @since 2.0.0
 */
class ObjectContract_Parser<T> {

    /**
     * Parsing stringified property name lists like <pre>"invoice desc, productCode, quantity"</pre>.
     * @param cls
     * @param propertyNames
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> ObjectContract<T> parse(Class<T> cls, @Nullable final String propertyNames) {

        Objects.requireNonNull(cls);

        final List<Clause<T>> clauses = _Strings.splitThenStream(propertyNames, ",")
                .map(String::trim)
                .filter(p->!p.isEmpty())
                .map(p->Clause.parse(cls, p))
                .collect(Collectors.toList());

        ObjectContract<T> contract = ObjectContract.empty(cls);

        if(clauses.isEmpty()) {
            return contract;
        }

        for(Clause<T> clause : clauses) {
            @SuppressWarnings("rawtypes")
            final Function valueExtractor = x->clause.extractValue((T)x);

            contract = contract.thenUse(
                    clause.propertyName,
                    valueExtractor,
                    clause.direction.getOrdering() );
        }

        return contract;
    }

    private static class Clause<T> {

        private static Pattern pattern =
                Pattern.compile("\\W*(\\w+)\\W*(asc|asc nullsFirst|asc nullsLast|desc|desc nullsFirst|desc nullsLast)?\\W*");

        private enum Direction {
            ASC {
                @Override @SuppressWarnings({ "unchecked", "rawtypes" })
                public Comparator<Comparable<?>> getOrdering() {
                    // legacy of Ordering.natural().nullsFirst();
                    return Comparator.nullsFirst(Comparator.<Comparable>naturalOrder());
                }
            },
            ASC_NULLS_LAST {
                @Override @SuppressWarnings({ "unchecked", "rawtypes" })
                public Comparator<Comparable<?>> getOrdering() {
                    // legacy of Ordering.natural().nullsLast();
                    return Comparator.nullsLast(Comparator.<Comparable>naturalOrder());
                }
            },
            DESC {
                @Override
                public Comparator<Comparable<?>> getOrdering() {
                    // legacy of Ordering.natural().nullsLast().reverse();
                    return ASC_NULLS_LAST.getOrdering().reversed();
                }
            },
            DESC_NULLS_LAST {
                @Override
                public Comparator<Comparable<?>> getOrdering() {
                    // legacy of Ordering.natural().nullsFirst().reverse();
                    return ASC.getOrdering().reversed();
                }
            };

            public abstract Comparator<Comparable<?>> getOrdering();

            public static Direction valueOfElseAsc(String str) {
                if("asc".equals(str)) return ASC;
                if("asc nullsFirst".equals(str)) return ASC;
                if("asc nullsLast".equals(str)) return ASC_NULLS_LAST;
                if("desc".equals(str)) return DESC;
                if("desc nullsFirst".equals(str)) return DESC;
                if("desc nullsLast".equals(str)) return DESC_NULLS_LAST;
                return ASC;
            }
        }

        private final Class<T> objectClass;
        private final String propertyName;
        private final Direction direction;
        private final Method getterMethod;

        private static <X> Clause<X> parse(Class<X> cls, String input) {
            final Matcher matcher = pattern.matcher(input);
            if(!matcher.matches()) {
                return null;
            }
            return new Clause<>(cls, matcher.group(1), Direction.valueOfElseAsc(matcher.group(2)));
        }

        private Clause(Class<T> cls, String propertyName, Direction direction) {
            this.objectClass = cls;
            this.propertyName = propertyName;
            this.direction = direction;
            this.getterMethod = findGetter();
        }

        public Method findGetter() {
            final String methodNameSuffix = _Strings.capitalize(propertyName);
            final String getMethodName = "get" + methodNameSuffix;
            try {
                return objectClass.getMethod(getMethodName);
            } catch (NoSuchMethodException e) {
                final String isMethodName = "is" + methodNameSuffix;
                try {
                    return objectClass.getMethod(isMethodName);
                } catch (NoSuchMethodException ex) {
                    throw new IllegalArgumentException("No such method ' " +
                            getMethodName + "' or '" + isMethodName + "'", e);
                }
            }
        }

        public Object extractValue(T obj) {
            try {
                return getterMethod.invoke(obj, _Constants.emptyObjects);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }

    }


}
