/*
 * 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.camel.component.facebook;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import facebook4j.Facebook;
import facebook4j.Reading;
import facebook4j.json.DataObjectFactory;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.facebook.data.FacebookMethodsType;
import org.apache.camel.component.facebook.data.FacebookMethodsTypeHelper.MatchType;
import org.apache.camel.component.facebook.data.FacebookPropertiesHelper;
import org.apache.camel.component.facebook.data.ReadingBuilder;
import org.apache.camel.support.ScheduledPollConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.camel.component.facebook.FacebookConstants.FACEBOOK_DATE_FORMAT;
import static org.apache.camel.component.facebook.FacebookConstants.READING_PREFIX;
import static org.apache.camel.component.facebook.FacebookConstants.READING_PROPERTY;
import static org.apache.camel.component.facebook.data.FacebookMethodsTypeHelper.filterMethods;
import static org.apache.camel.component.facebook.data.FacebookMethodsTypeHelper.getHighestPriorityMethod;
import static org.apache.camel.component.facebook.data.FacebookMethodsTypeHelper.getMissingProperties;
import static org.apache.camel.component.facebook.data.FacebookMethodsTypeHelper.invokeMethod;

/**
 * The Facebook consumer.
 */
public class FacebookConsumer extends ScheduledPollConsumer {

    private static final Logger LOG = LoggerFactory.getLogger(FacebookConsumer.class);

    private static final String SINCE_PREFIX = "since=";

    private final FacebookEndpoint endpoint;
    private final FacebookMethodsType method;
    private final Map<String, Object> endpointProperties;

    private String sinceTime;
    private String untilTime;

    public FacebookConsumer(FacebookEndpoint endpoint, Processor processor) {
        super(endpoint, processor);
        this.endpoint = endpoint;

        // determine the consumer method to invoke
        this.method = findMethod();

        // get endpoint properties in a map
        final HashMap<String, Object> properties = new HashMap<>();
        FacebookPropertiesHelper.getEndpointProperties(endpoint.getCamelContext(), endpoint.getConfiguration(), properties);

        // skip since and until fields?
        final Reading reading = (Reading) properties.get(READING_PROPERTY);
        if (reading != null) {
            final String queryString = reading.toString();
            if (queryString.contains(SINCE_PREFIX)) {
                // use the user supplied value to start with
                final int startIndex = queryString.indexOf(SINCE_PREFIX) + SINCE_PREFIX.length();
                int endIndex = queryString.indexOf('&', startIndex);
                if (endIndex == -1) {
                    // ignore the closing square bracket
                    endIndex = queryString.length() - 1;
                }
                final String strSince = queryString.substring(startIndex, endIndex);
                try {
                    this.sinceTime = URLDecoder.decode(strSince, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeCamelException(String.format("Error decoding %s.since with value %s due to: %s", READING_PREFIX, strSince, e.getMessage()), e);
                }
                LOG.debug("Using supplied property {}since value {}", READING_PREFIX, this.sinceTime);
            }
            if (queryString.contains("until=")) {
                LOG.debug("Overriding configured property {}until", READING_PREFIX);
            }
        }
        this.endpointProperties = Collections.unmodifiableMap(properties);
    }

    @Override
    public boolean isGreedy() {
        // make this consumer not greedy to avoid making too many Facebook calls
        return false;
    }

    private FacebookMethodsType findMethod() {

        FacebookMethodsType result;
        // find one that takes the largest subset of endpoint parameters
        final Set<String> argNames = new HashSet<>();
        argNames.addAll(FacebookPropertiesHelper.getEndpointPropertyNames(endpoint.getCamelContext(), endpoint.getConfiguration()));

        // add reading property for polling, if it doesn't already exist!
        argNames.add(READING_PROPERTY);

        final String[] argNamesArray = argNames.toArray(new String[argNames.size()]);
        List<FacebookMethodsType> filteredMethods = filterMethods(
            endpoint.getCandidates(), MatchType.SUPER_SET, argNamesArray);

        if (filteredMethods.isEmpty()) {
            throw new IllegalArgumentException(
                String.format("Missing properties for %s, need one or more from %s",
                    endpoint.getMethod(),
                    getMissingProperties(endpoint.getMethod(), endpoint.getNameStyle(), argNames)));
        } else if (filteredMethods.size() == 1) {
            // single match
            result = filteredMethods.get(0);
        } else {
            result = getHighestPriorityMethod(filteredMethods);
            LOG.warn("Using highest priority method {} from methods {}", method, filteredMethods);
        }
        return result;
    }

    @Override
    protected int poll() throws Exception {
        // invoke the consumer method
        final Map<String, Object> args = getMethodArguments();
        try {
            // also check whether we need to get raw JSON
            String rawJSON = null;
            Object result;
            if (endpoint.getConfiguration().getJsonStoreEnabled() == null
                || !endpoint.getConfiguration().getJsonStoreEnabled()) {
                result = invokeMethod(endpoint.getConfiguration().getFacebook(),
                    method, args);
            } else {
                final Facebook facebook = endpoint.getConfiguration().getFacebook();
                synchronized (facebook) {
                    result = invokeMethod(facebook, method, args);
                    rawJSON = DataObjectFactory.getRawJSON(result);
                }
            }

            // process result according to type
            if (result != null && (result instanceof Collection || result.getClass().isArray())) {
                // create an exchange for every element
                final Object array = getResultAsArray(result);
                final int length = Array.getLength(array);
                for (int i = 0; i < length; i++) {
                    processResult(Array.get(array, i), rawJSON);
                }
                return length;
            } else {
                processResult(result, rawJSON);
                return 1; // number of messages polled
            }
        } catch (Throwable t) {
            throw RuntimeCamelException.wrapRuntimeCamelException(t);
        }
    }

    private void processResult(Object result, String rawJSON) throws Exception {
        Exchange exchange = endpoint.createExchange();
        exchange.getIn().setBody(result);
        if (rawJSON != null) {
            exchange.getIn().setHeader(FacebookConstants.RAW_JSON_HEADER, rawJSON);
        }
        try {
            // send message to next processor in the route
            getProcessor().process(exchange);
        } finally {
            // log exception if an exception occurred and was not handled
            if (exchange.getException() != null) {
                getExceptionHandler().handleException("Error processing exchange", exchange, exchange.getException());
            }
        }
    }

    private Object getResultAsArray(Object result) {
        if (result.getClass().isArray()) {
            // no conversion needed
            return result;
        }
        // must be a Collection
        // TODO add support for Paging using ResponseList
        Collection<?> collection = (Collection<?>) result;
        return collection.toArray(new Object[collection.size()]);
    }

    private Map<String, Object> getMethodArguments() {
        // start by setting the Reading since and until fields,
        // these are used to avoid reading duplicate results across polls
        Map<String, Object> arguments = new HashMap<>();
        arguments.putAll(endpointProperties);

        Reading reading = (Reading) arguments.remove(READING_PROPERTY);
        if (reading == null) {
            reading = new Reading();
        } else {
            try {
                reading = ReadingBuilder.copy(reading, true);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new IllegalArgumentException(String.format("Error creating property [%s]: %s",
                        READING_PROPERTY, e.getMessage()), e);
            }
        }

        // now set since and until for this poll
        final SimpleDateFormat dateFormat = new SimpleDateFormat(FACEBOOK_DATE_FORMAT);
        final long currentMillis = System.currentTimeMillis();
        if (this.sinceTime == null) {
            // first poll, set this to (current time - initial poll delay)
            final Date startTime = new Date(currentMillis
                - TimeUnit.MILLISECONDS.convert(getInitialDelay(), getTimeUnit()));
            this.sinceTime = dateFormat.format(startTime);
        } else if (this.untilTime != null) {
            // use the last 'until' time
            this.sinceTime = this.untilTime;
        }
        this.untilTime = dateFormat.format(new Date(currentMillis));

        reading.since(this.sinceTime);
        reading.until(this.untilTime);

        arguments.put(READING_PROPERTY, reading);

        return arguments;
    }

}
