/**
 * Copyright (C) 2014-2017 Xavier Witdouck
 *
 * 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 com.zavtech.morpheus.util.text.parser;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;

import com.zavtech.morpheus.util.Asserts;
import com.zavtech.morpheus.util.functions.Function1;
import com.zavtech.morpheus.util.functions.FunctionStyle;
import com.zavtech.morpheus.util.functions.ToBooleanFunction;
import com.zavtech.morpheus.util.text.FormatException;

/**
 * A Function implementation of a Parser than can parse a String value into some another type.
 *
 * A design feature of the Parser framework is that it can be used to parse primitives while avoiding
 * boxing, which can make a difference when parsing very large files. The Function1 interface exposes
 * a number of applyXXX() methods that yield primitives, and a getStyle() method that can be queried
 * to check what type the function supports.
 *
 * @param <T>   the type produced by this parser
 *
 * <p><strong>This is open source software released under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 License</a></strong></p>
 *
 * @author  Xavier Witdouck
 */
public abstract class Parser<T> extends Function1<String,T> {

    private static final Set<String> defaultNullSet = new HashSet<>(Arrays.asList("null", "NULL", "Null", "N/A", "n/a", "-"));
    private static final ToBooleanFunction<String> defaultNullCheck = value -> value == null || value.trim().length() == 0 || defaultNullSet.contains(value);

    private Class<T> type;
    private ToBooleanFunction<String> nullChecker;

    /**
     * Constructor
     * @param style         the style for this function
     * @param type          the data type produced by this parser
     * @param nullChecker   the null checker function
     */
    public Parser(FunctionStyle style, Class<T> type, ToBooleanFunction<String> nullChecker) {
        super(style);
        this.type = type;
        this.nullChecker = nullChecker;
    }

    /**
     * Returns the data type produced by this parser
     * @return  the data type for this parser
     */
    public final Class<T> getType() {
        return type;
    }

    /**
     * Returns the null checker for this parser
     * @return      the null checker
     */
    public final ToBooleanFunction<String> getNullChecker() {
        return nullChecker;
    }

    /**
     * Applies the null checker function for this Parser
     * @param nullChecker   the null checker function
     * @return              this Parser
     */
    public Parser<T> withNullChecker(ToBooleanFunction<String> nullChecker) {
        Asserts.notNull(nullChecker, "The null checker cannot be null");
        this.nullChecker = nullChecker;
        return this;
    }

    /**
     * Returns true if this parser can process the value specified
     * @param value     the value to check if can be parsed by this parser
     * @return          true if the value can be parsed
     */
    public abstract boolean isSupported(String value);


    /**
     * Returns a possibly modified version of this parser optimized to parse values of the form presented
     * @param value     the format of the value to optimize for
     * @return          the newly created parser, or this parser if no optimizations can be done
     */
    public abstract Parser<T> optimize(String value);


    /**
     * Creates an BOOLEAN parser that wraps to function provided
     * @param function  the function to wrap
     * @return          the newly created parser
     */
    public static Parser<Boolean> forBoolean(ToBooleanFunction<String> function) {
        return new Parser<Boolean>(FunctionStyle.BOOLEAN, Boolean.class, v -> v == null) {
            @Override
            public final boolean applyAsBoolean(String value) {
                return value != null && function.applyAsBoolean(value);
            }
            @Override
            public Parser<Boolean> optimize(String value) {
                return this;
            }
            @Override
            public final boolean isSupported(String value) {
                return false;
            }
        };
    }

    /**
     * Creates an INTEGER parser that wraps to function provided
     * @param function  the function to wrap
     * @return          the newly created parser
     */
    public static Parser<Integer> forInt(ToIntFunction<String> function) {
        return new Parser<Integer>(FunctionStyle.INTEGER, Integer.class, v -> v == null) {
            @Override
            public final int applyAsInt(String value) {
                return value != null ? function.applyAsInt(value) : 0;
            }
            @Override
            public Parser<Integer> optimize(String value) {
                return this;
            }
            @Override
            public final boolean isSupported(String value) {
                return false;
            }
        };
    }

    /**
     * Creates an LONG parser that wraps to function provided
     * @param function  the function to wrap
     * @return          the newly created parser
     */
    public static Parser<Long> forLong(ToLongFunction<String> function) {
        return new Parser<Long>(FunctionStyle.LONG, Long.class, v -> v == null) {
            @Override
            public final long applyAsLong(String value) {
                return value != null ? function.applyAsLong(value) : 0L;
            }
            @Override
            public Parser<Long> optimize(String value) {
                return this;
            }
            @Override
            public final boolean isSupported(String value) {
                return false;
            }
        };
    }

    /**
     * Creates an DOUBLE parser that wraps to function provided
     * @param function  the function to wrap
     * @return          the newly created parser
     */
    public static Parser<Double> forDouble(ToDoubleFunction<String> function) {
        return new Parser<Double>(FunctionStyle.DOUBLE, Double.class, v -> v == null) {
            @Override
            public final double applyAsDouble(String value) {
                return value != null ? function.applyAsDouble(value) : Double.NaN;
            }
            @Override
            public Parser<Double> optimize(String value) {
                return this;
            }
            @Override
            public final boolean isSupported(String value) {
                return false;
            }
        };
    }

    /**
     * Creates an OBJECT parser that wraps to function provided
     * @param function  the function to wrap
     * @param type      the type for this parser
     * @param <O>       the output type
     * @return          the newly created parser
     */
    public static <O> Parser<O> forObject(Class<O> type, Function<String,O> function) {
        return new Parser<O>(FunctionStyle.OBJECT, type, v -> v == null) {
            @Override
            public final O apply(String value) {
                return value != null ? function.apply(value) : null;
            }
            @Override
            public Parser<O> optimize(String value) {
                return this;
            }
            @Override
            public final boolean isSupported(String value) {
                return false;
            }
        };
    }

    /**
     * Returns a newly created Parser for Boolean
     * @return  newly created Parser
     */
    public static Parser<Boolean> ofBoolean() {
        return new ParserOfBoolean(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for Integer
     * @return  newly created Parser
     */
    public static Parser<Integer> ofInteger() {
        return new ParserOfInteger(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for Long
     * @return  newly created Parser
     */
    public static Parser<Long> ofLong() {
        return new ParserOfLong(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for Double
     * @return  newly created Parser
     */
    public static Parser<Double> ofDouble() {
        return new ParserOfDouble(defaultNullCheck, Double::parseDouble);
    }

    /**
     * Returns a newly created Parser for Double
     * @param pattern   the decimal format pattern
     * @param multiplier    the multiplier to apply
     * @return  newly created Parser
     */
    public static Parser<Double> ofDouble(String pattern, int multiplier) {
        final DecimalFormat decimalFormat = createDecimalFormat(pattern, multiplier);
        return new ParserOfDouble(defaultNullCheck, value -> {
            try {
                return decimalFormat.parse(value);
            } catch (Exception ex) {
                throw new FormatException("Failed to parse value into double: " + value, ex);
            }
        });
    }

    /**
     * Returns a newly created Parser for BigDecimal
     * @return  newly created Parser
     */
    public static Parser<BigDecimal> ofBigDecimal() {
        return new ParserOfBigDecimal(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for an Enum class
     * @param enumClass the enum class
     * @return  newly created Parser
     */
    public static <T extends Enum> Parser<T> ofEnum(Class<T> enumClass) {
        return new ParserOfEnum<>(enumClass, defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for String
     * @return  newly created Parser
     */
    public static Parser<String> ofString() {
        return new ParserOfString(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for java.util.Date
     * @return  newly created Parser
     */
    public static Parser<Date> ofDate() {
        return new ParserOfDate<>(Date.class, defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for LocalTime
     * @return  newly created Parser
     */
    public static Parser<LocalTime> ofLocalTime(String pattern) {
        return ofLocalTime(DateTimeFormatter.ofPattern(pattern));
    }

    /**
     * Returns a newly created Parser for LocalTime
     * @param format    the date format
     * @return  newly created Parser
     */
    public static Parser<LocalTime> ofLocalTime(DateTimeFormatter format) {
        return new ParserOfLocalTime(defaultNullCheck, () -> format);
    }

    /**
     * Returns a newly created Parser for LocalDate
     * @param pattern   the date time format pattern
     * @return  newly created Parser
     */
    public static Parser<LocalDate> ofLocalDate(String pattern) {
        return ofLocalDate(DateTimeFormatter.ofPattern(pattern));
    }

    /**
     * Returns a newly created Parser for LocalDate
     * @param format    the date format
     * @return  newly created Parser
     */
    public static Parser<LocalDate> ofLocalDate(DateTimeFormatter format) {
        return new ParserOfLocalDate(defaultNullCheck, () -> format);
    }

    /**
     * Returns a newly created Parser for LocalDateTime
     * @param pattern   the date time format pattern
     * @return  newly created Parser
     */
    public static Parser<LocalDateTime> ofLocalDateTime(String pattern) {
        return ofLocalDateTime(DateTimeFormatter.ofPattern(pattern));
    }

    /**
     * Returns a newly created Parser for LocalDateTime
     * @param format    the date time format
     * @return  newly created Parser
     */
    public static Parser<LocalDateTime> ofLocalDateTime(DateTimeFormatter format) {
        return new ParserOfLocalDateTime(defaultNullCheck, () -> format);
    }

    /**
     * Returns a newly created Parser for ZoneDateTime
     * @param pattern   the date time format pattern
     * @param zoneId    the zone id
     * @return  newly created Parser
     */
    public static Parser<ZonedDateTime> ofZonedDateTime(String pattern, ZoneId zoneId) {
        return ofZonedDateTime(DateTimeFormatter.ofPattern(pattern).withZone(zoneId));
    }

    /**
     * Returns a newly created Parser for ZoneDateTime
     * @param format    the date time format
     * @return  newly created Parser
     */
    public static Parser<ZonedDateTime> ofZonedDateTime(DateTimeFormatter format) {
        return new ParserOfZonedDateTime(defaultNullCheck, () -> format);
    }

    /**
     * Returns a newly created Parser for Period
     * @return  newly created Parser
     */
    public static Parser<Period> ofPeriod() {
        return new ParserOfPeriod(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for TimeZone
     * @return  newly created Parser
     */
    public static Parser<TimeZone> ofTimeZone() {
        return new ParserOfTimeZone(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for ZoneId
     * @return  newly created Parser
     */
    public static Parser<ZoneId> ofZoneId() {
        return new ParserOfZoneId(defaultNullCheck);
    }

    /**
     * Returns a newly created Parser for Object
     * @return  newly created Parser
     */
    public static Parser<Object> ofObject() {
        return new ParserOfObject(defaultNullCheck);
    }

    /**
     * Returns a newly created DecimalFormat object
     * @param pattern       the format pattern
     * @param multiplier    the multiplier
     * @return              the formatter
     */
    private static DecimalFormat createDecimalFormat(String pattern, int multiplier) {
        final DecimalFormat decimalFormat = new DecimalFormat(pattern);
        decimalFormat.setMultiplier(multiplier);
        return decimalFormat;
    }

}