/*
 * 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.jmeter.report.processor;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.commons.lang3.Validate;
import org.apache.jmeter.report.core.CsvSampleReader;
import org.apache.jmeter.report.core.Sample;
import org.apache.jmeter.report.core.SampleException;
import org.apache.jmeter.report.core.SampleMetadata;
import org.apache.jmeter.report.core.TimeHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Read a csv source file and write its rows (samples) all the registered
 * <code>SampleConsumer</code>s.<br>
 * If there is several other source files with the same root name then those
 * files are produced on their corresponding channels.<br>
 * <p>
 * The root name of the files is determined by the source file name and is made
 * of its name without the file extension :<br>
 * <b>Example:</b> If <code>results.csv</code> is the source file name then
 * <code>results</code> is the root file name.<br>
 * <br>
 * The <code>CsvFileSampleSource</code> looks for all the files in the same
 * directory of the main source file that have the same root file name<br>
 * <b>Example</b> : if the directory contains <code>results.csv</code>,
 * <code>results-1.csv</code>, <code>results-2.csv</code>, etc. then all
 * these files will be read and produced on their corresponding channels.<br>
 * The channel on which an input file will be produce is determined by its
 * suffix<br>
 * <ul>
 * <li>If the input file is named <code>results-1.csv</code> then it will
 * be produced on the channel 1.</li>
 * <li>If the input file is named <code>results-2.csv</code> then it will
 * be produced on the channel 2.</li>
 * <li>If the input file is named <code>results.csv</code> then it will
 * be produced on the channel 0.</li>
 * </ul>
 *
 * @since 3.0
 */
public class CsvFileSampleSource extends AbstractSampleSource {

    /** File name whose sample are being produced on the channel */
    public static final String SOURCE_FILE_ATTRIBUTE = "samplesource.file";

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

    /** input csv files to be produced */
    private final File[] inputFiles;

    /** csv readers corresponding to the input files */
    private final CsvSampleReader[] csvReaders;

    /** mock producer to produce samples to its consumers */
    private final PrivateProducer producer;

    /**
     * Build a sample source from the specified input file and character
     * separator.
     *
     * @param inputFile The input sample file (CSV file) (must not be {@code null})
     * @param separator The character separator to be used for delimiting samples
     *                  columns
     */
    public CsvFileSampleSource(final File inputFile, final char separator) {
        final String inputRootName = getFileRootName(inputFile.getName());
        final String inputExtension = getFileExtension(inputFile.getName());

        // Find secondary inputs by regex match
        File[] secondaryInputs = null;
        try {
            final Pattern pattern = Pattern.compile(inputRootName
                    + "-[0-9]+\\." + inputExtension);
            secondaryInputs = inputFile.getAbsoluteFile().getParentFile()
                    .listFiles(pathname -> pathname.isFile()
                            && pattern.matcher(pathname.getName()).matches());
        } catch (PatternSyntaxException e) {
            throw new SampleException("Could not locate input sample files !",
                    e);
        }
        if (secondaryInputs == null) {
            secondaryInputs = new File[0];
        }
        inputFiles = new File[secondaryInputs.length + 1];
        csvReaders = new CsvSampleReader[secondaryInputs.length + 1];
        int k = 0;
        // primary input file (ex. input.csv)
        csvReaders[k] = new CsvSampleReader(inputFile, separator, true);
        inputFiles[k] = inputFile;
        // secondary input files (ex. input-1.csv, input-2.csv, input-3.csv)
        for (File input : secondaryInputs) {
            k++;
            csvReaders[k] = new CsvSampleReader(input, separator, true);
            inputFiles[k] = secondaryInputs[k - 1];
        }
        producer = new PrivateProducer();
    }

    private static String getFileRootName(String fName) {
        int idx = fName.lastIndexOf('.');
        if (idx < 0) {
            return fName;
        }
        return fName.substring(0, idx);
    }

    private static String getFileExtension(String fName) {
        int idx = fName.lastIndexOf('.');
        if (idx < 0) {
            return "";
        }
        if (idx < fName.length() - 1) {
            return fName.substring(idx + 1);
        }
        return "";
    }

    /**
     * Get the current time in milliseconds
     */
    private static long now() {
        return System.currentTimeMillis();
    }

    /**
     * Get a readable time as hours, minutes and seconds from the specified time
     * in milliseconds
     *
     * @return A readable string that displays the time provided as milliseconds
     */
    private static String time(long t) {
        return TimeHelper.time(t);
    }

    /**
     * Read all input CSV files and produce their samples on registered sample
     * consumers
     */
    private void produce() {
        SampleContext context = getSampleContext();
        Validate.validState(context != null, "Set a sample context before producing samples.");

        for (int i = 0; i < csvReaders.length; i++) {
            long sampleCount = 0;
            long start = now();
            CsvSampleReader csvReader = csvReaders[i];
            producer.setSampleContext(context);
            producer.setProducedMetadata(csvReader.getMetadata(), i);
            producer.setChannelAttribute(i, SOURCE_FILE_ATTRIBUTE,
                    inputFiles[i]);
            producer.startProducing();
            try {
                Sample s = null;
                while ((s = csvReader.readSample()) != null) {
                    producer.produce(s, i);
                    sampleCount++;
                }
            } finally {
                producer.stopProducing();
                csvReader.close();
            }
            if (LOG.isInfoEnabled()) {
                LOG.info("produce(): {} samples produced in {} on channel {}",
                        sampleCount, time(now() - start), i);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.jmeter.report.processor.AbstractSampleSource#addSampleConsumers
     * (java.util.List)
     */
    @Override
    public void setSampleConsumers(List<SampleConsumer> consumers) {
        producer.setSampleConsumers(consumers);
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.jmeter.report.processor.AbstractSampleSource#addSampleConsumer
     * (org.apache.jmeter.report.processor.SampleConsumer)
     */
    @Override
    public void addSampleConsumer(SampleConsumer consumer) {
        producer.addSampleConsumer(consumer);
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.jmeter.report.processor.AbstractSampleSource#removeSampleConsumer
     * (org.apache.jmeter.report.processor.SampleConsumer)
     */
    @Override
    public void removeSampleConsumer(SampleConsumer consumer) {
        producer.removeSampleConsumer(consumer);
    }

    /**
     * Run this sample source.<br>
     * This sample source will start reading all inputs CSV files and produce
     * their samples to this sample source registered sample consumers.
     */
    @Override
    public void run() {
        produce();
    }

    private static class PrivateProducer extends AbstractSampleProcessor implements
            SampleProducer {

        private List<SampleConsumer> sampleConsumers = new ArrayList<>();

        /**
         * Set the consumers for the samples that are to be consumed
         *
         * @param consumers list of consumers for the samples (must not be
         *                  {@code null})
         */
        public void setSampleConsumers(List<SampleConsumer> consumers) {
            Objects.requireNonNull(consumers, "consumers must not be null");

            this.sampleConsumers = consumers;
        }

        public void addSampleConsumer(SampleConsumer consumer) {
            if (consumer == null) {
                return;
            }
            this.sampleConsumers.add(consumer);
        }

        public void removeSampleConsumer(SampleConsumer consumer) {
            if (consumer == null) {
                return;
            }
            this.sampleConsumers.remove(consumer);
        }

        @Override
        public void setSampleContext(SampleContext context) {
            for (SampleConsumer consumer : this.sampleConsumers) {
                try {
                    consumer.setSampleContext(context);
                } catch (Exception e) {
                    throw new SampleException("Consumer failed with message :"
                            + e.getMessage(), e);
                }
            }
        }

        @Override
        public void setProducedMetadata(SampleMetadata metadata, int channel) {
            for (SampleConsumer consumer : this.sampleConsumers) {
                try {
                    consumer.setConsumedMetadata(metadata, channel);
                } catch (Exception e) {
                    throw new SampleException("Consumer failed with message :"
                            + e.getMessage(), e);
                }
            }
        }

        @Override
        public void setChannelAttribute(int channel, String key, Object value) {
            super.setChannelAttribute(channel, key, value);
            // propagate to this mock producer's consumers
            for (SampleConsumer consumer : this.sampleConsumers) {
                try {
                    consumer.setChannelAttribute(channel, key, value);
                } catch (Exception e) {
                    throw new SampleException("Consumer failed with message :"
                            + e.getMessage(), e);
                }
            }
        }

        @Override
        public void startProducing() {
            for (SampleConsumer consumer : this.sampleConsumers) {
                try {
                    consumer.startConsuming();
                } catch (Exception e) {
                    throw new SampleException("Consumer failed with message :"
                            + e.getMessage(), e);
                }
            }
        }

        @Override
        public void produce(Sample s, int channel) {
            for (SampleConsumer consumer : this.sampleConsumers) {
                try {
                    consumer.consume(s, channel);
                } catch (Exception e) {
                    throw new SampleException("Consumer failed with message :"
                            + e.getMessage(), e);
                }
            }
        }

        @Override
        public void stopProducing() {
            for (SampleConsumer consumer : this.sampleConsumers) {
                try {
                    consumer.stopConsuming();
                } catch (Exception e) {
                    throw new SampleException("Consumer failed with message :"
                            + e.getMessage(), e);
                }
            }
        }
    }

}
