/*
 * Copyright 2016 Stormpath, Inc.
 *
 * 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.stormpath.sdk.impl.resource;

import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.stormpath.sdk.impl.ds.Enlistment;
import com.stormpath.sdk.lang.Assert;
import com.stormpath.sdk.lang.Classes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Refactored methods from {@link AbstractResource} to make them common to subclasses.
 *
 * @since 1.2.0
 */
public abstract class AbstractPropertyRetriever {

    private static final Logger log = LoggerFactory.getLogger(AbstractPropertyRetriever.class);

    private static final DateFormat dateFormatter = new ISO8601DateFormat();

    protected final Lock readLock;

    protected final Lock writeLock;


    protected AbstractPropertyRetriever() {
        ReadWriteLock rwl = new ReentrantReadWriteLock();
        this.readLock = rwl.readLock();
        this.writeLock = rwl.writeLock();
    }

    public abstract Object getProperty(String name);

    /**
     * @since 0.8
     */
    protected String getString(StringProperty property) {
        return getStringProperty(property.getName());
    }

    protected String getStringProperty(String key) {
        Object value = getProperty(key);
        if (value == null) {
            return null;
        }
        return String.valueOf(value);
    }


    /**
     * @since 0.8
     */
    protected int getInt(IntegerProperty property) {
        return getIntProperty(property.getName());
    }

    protected int getIntProperty(String key) {
        Object value = getProperty(key);
        if (value != null) {
            if (value instanceof String) {
                return parseInt((String) value);
            } else if (value instanceof Number) {
                return ((Number) value).intValue();
            }
        }
        return -1;
    }

    /**
     * @since 0.9
     */
    protected boolean getBoolean(BooleanProperty property) {
        return getBooleanProperty(property.getName());
    }

    /**
     * Returns an actual boolean value instead of a possible null Boolean value since desired usage
     * is to have either a true or false.
     *
     * @since 0.9
     */
    protected boolean getBooleanProperty(String key) {
        return getNullableBooleanProperty(key) == Boolean.TRUE;
    }

    protected Boolean getNullableBoolean(BooleanProperty property) {
        return getNullableBooleanProperty(property.getName());
    }

    protected Boolean getNullableBooleanProperty(String key) {
        Object value = getProperty(key);
        if (value != null) {
            if (value instanceof Boolean) {
                return (Boolean) value;
            } else if (value instanceof String) {
                return Boolean.valueOf((String) value);
            }
        }
        return null;
    }

    private int parseInt(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            if (log.isErrorEnabled()) {
                String msg = "Unabled to parse string '{}' into an integer value.  Defaulting to -1";
                log.error(msg, e);
            }
        }
        return -1;
    }

    /**
     * @since 0.8
     */
    protected Date getDateProperty(DateProperty key) {
        Object value = getProperty(key.getName());
        if (value == null) {
            return null;
        }

        try {
            return dateFormatter.parse(String.valueOf(value));
        } catch (ParseException e) {
            if (log.isErrorEnabled()) {
                String msg = "Unabled to parse string '{}' into an date value.  Defaulting to null.";
                log.error(msg, e);
            }
        }
        return null;
    }

    /**
     * Returns the {@link List} property identified by {@code key}
     *
     * @since 1.0.RC8
     */
    protected List getListProperty(String key) {
        Object list = getProperty(key);
        return (List) list;
    }

    /**
     * Returns the {@link Set} property identified by {@code key}
     *
     * @since 1.0.RC8
     */
    protected Set getSetProperty(String key) {
        Object set = getProperty(key);
        return (Set) set;
    }

    /**
     * @since 1.0.RC4
     */
    protected Map getMap(MapProperty mapProperty) {
        return getMapProperty(mapProperty.getName());
    }

    /**
     * @since 1.0.RC4
     */
    protected Map getMapProperty(String key) {
        Object value = getProperty(key);
        if (value != null) {
            if (value instanceof Map) {
                return (Map) value;
            }
            String msg = "'" + key + "' property value type does not match the specified type. Specified type: Map. " +
                    "Existing type: " + value.getClass().getName();
            msg += (isPrintableProperty(key) ? ".  Value: " + value : ".");
            throw new IllegalArgumentException(msg);
        }
        return null;
    }

    protected <E extends Enum<E>> E getEnumProperty(EnumProperty<E> enumProperty) {
        return getEnumProperty(enumProperty.getName(), enumProperty.getType());
    }

    protected <E extends Enum<E>> E getEnumProperty(String key, Class<E> type) {
        Assert.notNull(type, "type cannot be null.");

        Object value = getProperty(key);

        if (value != null) {
            if (value instanceof String) {
                return Enum.valueOf(type, value.toString());
            }
            if (type.isAssignableFrom(value.getClass())) {
                //noinspection unchecked
                return (E) value;
            }
        }
        return null;
    }

    protected <T, P> T getParentAwareObjectProperty(ParentAwareObjectProperty<T, P> objectProperty) {
        return getParentAwareObjectProperty(objectProperty.getName(), objectProperty.getType(), objectProperty.getParentType());
    }

    protected <T, P> T getParentAwareObjectProperty(String name, Class<T> type, Class<P> parentType) {

        Object value = getProperty(name);

        if (value == null) {
            return null;
        }

        if (type.isAssignableFrom(value.getClass())) {
            return (T) value;
        }

        if (value instanceof Map) {
            writeLock.lock();
            try {
                Constructor<T> propertyConstructor = Classes.getConstructor(type, String.class, Map.class, parentType);

                @SuppressWarnings("unchecked")
                T instance = propertyConstructor.newInstance(name, new Enlistment((Map<String, Object>) value) , this);

                getProperties().put(name, instance);

                return instance;
            } catch (Exception e) {
                throw new IllegalArgumentException("Unable to create instace", e);
            } finally {
                writeLock.unlock();
            }
        }

        String msg = "'" + name + "' property value type does not match the specified property type. " +
                "Existing type: " + value.getClass().getName();

        msg += (isPrintableProperty(name) ? ".  Value: " + value : ".");

        throw new IllegalArgumentException(msg);
    }

    /**
     * Returns {@code true} if the internal property is safe to print in toString(), {@code false} otherwise.
     *
     * @param name The name of the property to check for safe printing
     * @return {@code true} if the internal property is safe to print in toString(), {@code false} otherwise.
     * @since 0.4.1
     */
    protected boolean isPrintableProperty(String name) {
        return true;
    }

    /**
     * @since 0.8
     */
    protected void setProperty(Property property, Object value) {
        setProperty(property.getName(), value, true);
    }

    public void setProperty(String name, Object value) {
        setProperty(name, value, true);
    }

    /**
     * @since 0.6.0
     */
    protected abstract Object setProperty(String name, Object value, final boolean dirty);

    /**
     * @since 1.2.0
     */
    protected abstract Map<String, Object> getProperties();

}
