/*
 * *************************************************************************************
 *  Copyright (C) 2008 EsperTech, Inc. All rights reserved.                            *
 *  http://esper.codehaus.org                                                          *
 *  http://www.espertech.com                                                           *
 *  ---------------------------------------------------------------------------------- *
 *  The software in this package is published under the terms of the GPL license       *
 *  a copy of which has been included with this distribution in the license.txt file.  *
 * *************************************************************************************
 */

package com.espertech.esper.dataflow.ops;

import com.espertech.esper.client.EventBean;
import com.espertech.esper.client.EventType;
import com.espertech.esper.client.dataflow.EPDataFlowSignalFinalMarker;
import com.espertech.esper.core.service.StatementContext;
import com.espertech.esper.dataflow.annotations.DataFlowContext;
import com.espertech.esper.dataflow.annotations.DataFlowOpParameter;
import com.espertech.esper.dataflow.annotations.DataFlowOperator;
import com.espertech.esper.dataflow.interfaces.*;
import com.espertech.esper.dataflow.util.GraphTypeDesc;
import com.espertech.esper.epl.expression.*;
import com.espertech.esper.event.EventBeanManufacturer;
import com.espertech.esper.event.EventTypeUtility;
import com.espertech.esper.event.WriteablePropertyDescriptor;
import com.espertech.esper.util.TypeWidener;
import com.espertech.esper.util.TypeWidenerFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.*;

@DataFlowOperator
public class BeaconSource implements DataFlowSourceOperator {
    private static final Log log = LogFactory.getLog(BeaconSource.class);

    private final static List<String> PARAMETER_PROPERTIES = Arrays.asList("interval", "iterations", "initialDelay");

    @DataFlowContext
    private EPDataFlowEmitter graphContext;

    @DataFlowOpParameter
    private long iterations;

    @DataFlowOpParameter
    private double initialDelay;

    @DataFlowOpParameter
    private double interval;

    private Map<String, Object> allProperties = new LinkedHashMap<String, Object>();

    private long initialDelayMSec;
    private long periodDelayMSec;
    private long lastSendTime;
    private long iterationNumber;
    private boolean produceEventBean;

    private ExprEvaluator[] evaluators;
    private EventBeanManufacturer manufacturer;

    @DataFlowOpParameter(all=true)
    public void setProperty(String name, Object value) {
        allProperties.put(name, value);
    }

    public DataFlowOpInitializeResult initialize(DataFlowOpInitializateContext context) throws Exception {
        initialDelayMSec = (long) (initialDelay * 1000);
        periodDelayMSec = (long) (interval * 1000);

        if (context.getOutputPorts().size() != 1) {
            throw new IllegalArgumentException("BeaconSource operator requires one output stream but produces " + context.getOutputPorts().size() + " streams");
        }

        // Check if a type is declared
        DataFlowOpOutputPort port = context.getOutputPorts().get(0);
        if (port.getOptionalDeclaredType() != null && port.getOptionalDeclaredType().getEventType() != null) {
            EventType outputEventType = port.getOptionalDeclaredType().getEventType();
            produceEventBean = port.getOptionalDeclaredType() != null && !port.getOptionalDeclaredType().isUnderlying();

            // compile properties to populate
            Set<String> props = allProperties.keySet();
            props.removeAll(PARAMETER_PROPERTIES);
            WriteablePropertyDescriptor[] writables = setupProperties(props.toArray(new String[props.size()]), outputEventType, context.getStatementContext());
            manufacturer = context.getServicesContext().getEventAdapterService().getManufacturer(outputEventType, writables, context.getServicesContext().getEngineImportService(), false);

            int index = 0;
            evaluators = new ExprEvaluator[writables.length];
            for (WriteablePropertyDescriptor writeable : writables) {

                final Object providedProperty = allProperties.get(writeable.getPropertyName());
                if (providedProperty instanceof ExprNode) {
                    ExprNode exprNode = (ExprNode) providedProperty;
                    ExprNode validated = ExprNodeUtility.validateSimpleGetSubtree(exprNode, context.getStatementContext(), null);
                    final ExprEvaluator exprEvaluator = validated.getExprEvaluator();
                    final TypeWidener widener = TypeWidenerFactory.getCheckPropertyAssignType(validated.toExpressionString(), exprEvaluator.getType(),
                            writeable.getType(), writeable.getPropertyName());
                    if (widener != null) {
                        evaluators[index] = new ExprEvaluator() {
                            public Object evaluate(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext context) {
                                Object value = exprEvaluator.evaluate(eventsPerStream, isNewData, context);
                                return widener.widen(value);
                            }
                            public Class getType() {
                                return null;
                            }
                        };
                    }
                    else {
                        evaluators[index] = exprEvaluator;
                    }
                }
                else if (providedProperty == null) {
                    evaluators[index] = new ExprEvaluator() {
                        public Object evaluate(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext context) {
                            return null;
                        }
                        public Class getType() {
                            return null;
                        }
                    };
                }
                else {
                    evaluators[index] = new ExprEvaluator() {
                        public Object evaluate(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext context) {
                            return providedProperty;
                        }
                        public Class getType() {
                            return providedProperty.getClass();
                        }
                    };
                }
                index++;
            }

            return null;    // no changing types
        }

        // No type has been declared, we can create one
        String anonymousTypeName = context.getDataflowName() + "-beacon";
        Map<String, Object> types = new LinkedHashMap<String, Object>();
        Set<String> props = allProperties.keySet();
        props.removeAll(PARAMETER_PROPERTIES);

        int count = 0;
        evaluators = new ExprEvaluator[props.size()];
        for (String propertyName : props) {
            ExprNode exprNode = (ExprNode) allProperties.get(propertyName);
            ExprNode validated = ExprNodeUtility.validateSimpleGetSubtree(exprNode, context.getStatementContext(), null);
            final Object value = validated.getExprEvaluator().evaluate(null, true, context.getAgentInstanceContext());
            if (value == null) {
                types.put(propertyName, null);
            }
            else {
                types.put(propertyName, value.getClass());
            }
            evaluators[count] = new ExprEvaluator() {
                public Object evaluate(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext context) {
                    return value;
                }
                public Class getType() {
                    return null;
                }
            };
            count++;
        }
        
        EventType type = context.getServicesContext().getEventAdapterService().createAnonymousObjectArrayType(anonymousTypeName, types);
        return new DataFlowOpInitializeResult(new GraphTypeDesc[] {new GraphTypeDesc(false, true, type)});
    }

    public void next() {
        if (iterationNumber == 0 && initialDelayMSec > 0) {
            try {
                Thread.sleep(initialDelayMSec, 0);
            }
            catch (InterruptedException e) {
                graphContext.submitSignal(new EPDataFlowSignalFinalMarker() {});
            }
        }

        if (iterationNumber > 0 && periodDelayMSec > 0) {
            long nsecDelta = lastSendTime - System.nanoTime();
            long sleepTime = periodDelayMSec - nsecDelta / 1000000;
            if (sleepTime > 0) {
                try {
                    Thread.sleep(sleepTime);
                }
                catch (InterruptedException e) {
                    graphContext.submitSignal(new EPDataFlowSignalFinalMarker() {});
                }
            }
        }

        if (iterations > 0 && iterationNumber >= iterations) {
            graphContext.submitSignal(new EPDataFlowSignalFinalMarker() {});
        }
        else {
            iterationNumber++;
            if (evaluators != null) {
                Object[] row = new Object[evaluators.length];
                for (int i = 0; i < row.length; i++) {
                    if (evaluators[i] != null) {
                        row[i] = evaluators[i].evaluate(null, true, null);
                    }
                }
                if (log.isDebugEnabled()) {
                    log.debug("BeaconSource submitting row " + Arrays.toString(row));
                }

                Object outputEvent = row;
                if (manufacturer != null) {
                    if (!produceEventBean) {
                        outputEvent = manufacturer.makeUnderlying(row);
                    }
                    else {
                        outputEvent = manufacturer.make(row);
                    }
                }
                graphContext.submit(outputEvent);
            }
            else {
                if (log.isDebugEnabled()) {
                    log.debug("BeaconSource submitting empty row");
                }
                graphContext.submit(new Object[0]);
            }

            if (interval > 0) {
                lastSendTime = System.nanoTime();
            }
        }
    }

    public void open(DataFlowOpOpenContext openContext) {
        // no action
    }

    public void close(DataFlowOpCloseContext openContext) {
        // no action
    }

    private static WriteablePropertyDescriptor[] setupProperties(String[] propertyNamesOffered, EventType outputEventType, StatementContext statementContext)
            throws ExprValidationException
    {
        Set<WriteablePropertyDescriptor> writeables = statementContext.getEventAdapterService().getWriteableProperties(outputEventType, false);

        List<WriteablePropertyDescriptor> writablesList = new ArrayList<WriteablePropertyDescriptor>();

        for (int i = 0; i < propertyNamesOffered.length; i++) {
            String propertyName = propertyNamesOffered[i];
            WriteablePropertyDescriptor writable = EventTypeUtility.findWritable(propertyName, writeables);
            if (writable == null) {
                throw new ExprValidationException("Failed to find writable property '" + propertyName + "' for event type '" + outputEventType.getName() + "'");
            }
            writablesList.add(writable);
        }

        return writablesList.toArray(new WriteablePropertyDescriptor[writablesList.size()]);
    }
}
