/*
  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

      https://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.commons.cli;

import java.io.File;
import java.io.FileInputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.nio.file.Path;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * TypeHandler will handle the pluggable conversion and verification of Option types. It handles the mapping of classes to bot converters and verifiers. It
 * provides the default conversion and verification methods when converters and verifiers are not explicitly set.
 * <p>
 * If Options are serialized and deserialized their converters and verifiers will revert to the defaults defined in this class. To correctly de-serialize
 * Options with custom converters and/or verifiers, using the default serialization methods, this class should be properly configured with the custom converters
 * and verifiers for the specific class.
 * </p>
 */
public class TypeHandler {

    /**
     * The default TypeHandler.
     */
    private static final TypeHandler DEFAULT = new TypeHandler();

    /** Value of hex conversion of strings */
    private static final int HEX_RADIX = 16;

    /**
     * Returns the class whose name is {@code className}.
     *
     * @param className the class name
     * @return The class if it is found
     * @throws ParseException if the class could not be found
     */
    public static Class<?> createClass(final String className) throws ParseException {
        return createValue(className, Class.class);
    }

    /**
     * Returns the date represented by {@code string}.
     * <p>
     * This method is not yet implemented and always throws an {@link UnsupportedOperationException}.
     * </p>
     *
     * @param string the date string
     * @return The date if {@code string} is a valid date string, otherwise return null.
     */
    public static Date createDate(final String string) {
        return createValueUnchecked(string, Date.class);
    }

    /**
     * Creates a default converter map.
     *
     * @return a default converter map.
     * @since 1.7.0
     */
    public static Map<Class<?>, Converter<?, ? extends Throwable>> createDefaultMap() {
        return putDefaultMap(new HashMap<>());
    }

    /**
     * Returns the File represented by {@code string}.
     *
     * @param string the File location
     * @return The file represented by {@code string}.
     */
    public static File createFile(final String string) {
        return createValueUnchecked(string, File.class);
    }

    /**
     * Creates the File[] represented by {@code string}.
     *
     * <p>
     * This method is not yet implemented and always throws an {@link UnsupportedOperationException}.
     * </p>
     *
     * @param string the paths to the files
     * @return The File[] represented by {@code string}.
     * @throws UnsupportedOperationException always
     * @deprecated with no replacement
     */
    @Deprecated // since 1.7.0
    public static File[] createFiles(final String string) {
        // to implement/port:
        // return FileW.findFiles(string);
        throw new UnsupportedOperationException("Not yet implemented");
    }

    /**
     * Creates a number from a String. If a '.' is present, it creates a Double, otherwise a Long.
     *
     * @param string the value
     * @return the number represented by {@code string}
     * @throws ParseException if {@code string} is not a number
     */
    @Deprecated // since 1.7.0
    public static Number createNumber(final String string) throws ParseException {
        return createValue(string, Number.class);
    }

    /**
     * Creates an Object from the class name and empty constructor.
     *
     * @param className the argument value
     * @return the initialized object
     * @throws ParseException if the class could not be found or the object could not be created
     * @deprecated use {@link #createValue(String, Class)}
     */
    @Deprecated // since 1.7.0
    public static Object createObject(final String className) throws ParseException {
        return createValue(className, Object.class);
    }

    /**
     * Creates the URL represented by {@code string}.
     *
     * @param string the URL string
     * @return The URL in {@code string} is well-formed
     * @throws ParseException if the URL in {@code string} is not well-formed
     */
    public static URL createURL(final String string) throws ParseException {
        return createValue(string, URL.class);
    }

    /**
     * Creates the @code Object} of type {@code clazz} with the value of {@code string}.
     *
     * @param string the command line value
     * @param clazz  the class representing the type of argument
     * @param <T>    type of argument
     * @return The instance of {@code clazz} initialized with the value of {@code string}.
     * @throws ParseException if the value creation for the given class threw an exception.
     */
    public static <T> T createValue(final String string, final Class<T> clazz) throws ParseException {
        try {
            return getDefault().getConverter(clazz).apply(string);
        } catch (final Exception e) {
            throw ParseException.wrap(e);
        }
    }

    /**
     * Creates the {@code Object} of type {@code obj} with the value of {@code string}.
     *
     * @param string the command line value
     * @param obj    the type of argument
     * @return The instance of {@code obj} initialized with the value of {@code string}.
     * @throws ParseException if the value creation for the given object type failed
     * @deprecated use {@link #createValue(String, Class)}
     */
    @Deprecated // since 1.7.0
    public static Object createValue(final String string, final Object obj) throws ParseException {
        return createValue(string, (Class<?>) obj);
    }

    /**
     * Delegates to {@link #createValue(String, Class)} throwing IllegalArgumentException instead of ParseException.
     *
     * @param string the command line value
     * @param clazz  the class representing the type of argument
     * @param <T>    type of argument
     * @return The instance of {@code clazz} initialized with the value of {@code string}.
     * @throws IllegalArgumentException if the value creation for the given class threw an exception.
     */
    private static <T> T createValueUnchecked(final String string, final Class<T> clazz) {
        try {
            return createValue(string, clazz);
        } catch (final ParseException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Gets the default TypeHandler.
     *
     * @return the default TypeHandler.
     * @since 1.7.0
     */
    public static TypeHandler getDefault() {
        return DEFAULT;
    }

    /**
     * Returns the opened FileInputStream represented by {@code string}.
     *
     * @param string the file location
     * @return The file input stream represented by {@code string}.
     * @throws ParseException if the file is not exist or not readable
     * @deprecated use {@link #createValue(String, Class)}
     */
    @Deprecated // since 1.7.0
    public static FileInputStream openFile(final String string) throws ParseException {
        return createValue(string, FileInputStream.class);
    }

    private static Map<Class<?>, Converter<?, ? extends Throwable>> putDefaultMap(final Map<Class<?>, Converter<?, ? extends Throwable>> map) {
        map.put(Object.class, Converter.OBJECT);
        map.put(Class.class, Converter.CLASS);
        map.put(Date.class, Converter.DATE);
        map.put(File.class, Converter.FILE);
        map.put(Path.class, Converter.PATH);
        map.put(Number.class, Converter.NUMBER);
        map.put(URL.class, Converter.URL);
        map.put(FileInputStream.class, FileInputStream::new);
        map.put(Long.class, Long::parseLong);
        map.put(Integer.class, Integer::parseInt);
        map.put(Short.class, Short::parseShort);
        map.put(Byte.class, Byte::parseByte);
        map.put(Character.class, s -> s.startsWith("\\u") ? Character.toChars(Integer.parseInt(s.substring(2), HEX_RADIX))[0] : s.charAt(0));
        map.put(Double.class, Double::parseDouble);
        map.put(Float.class, Float::parseFloat);
        map.put(BigInteger.class, BigInteger::new);
        map.put(BigDecimal.class, BigDecimal::new);
        return map;
    }

    /**
     * Map of Class to Converter.
     * <p>
     * For each entry, that Class' type must match the Converter's first type.
     * </p>
     */
    private final Map<Class<?>, Converter<?, ? extends Throwable>> converterMap;

    /**
     * Constructs a default initialized instance.
     */
    public TypeHandler() {
        this(createDefaultMap());
    }

    /**
     * Constructs a default initialized instance.
     * <p>
     * For each entry, that Class' type must match the Converter's first type.
     * </p>
     *
     * @param converterMap The converter map, not null.
     * @since 1.7.0
     */
    public TypeHandler(final Map<Class<?>, Converter<?, ? extends Throwable>> converterMap) {
        this.converterMap = Objects.requireNonNull(converterMap, "converterMap");
    }

    /**
     * Gets the registered converter for the the Class, or {@link Converter#DEFAULT} if absent.
     *
     * @param <T>   The Class parameter type.
     * @param clazz The Class to get the Converter for.
     * @return the registered converter if any, {@link Converter#DEFAULT} otherwise.
     * @since 1.7.0
     */
    @SuppressWarnings("unchecked") // returned value will have type T because it is fixed by clazz
    public <T> Converter<T, ?> getConverter(final Class<T> clazz) {
        return (Converter<T, ?>) converterMap.getOrDefault(clazz, Converter.DEFAULT);
    }

}
