package edu.sc.seis.sod.source.network;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.w3c.dom.Element;

import com.csvreader.CsvReader;

import edu.iris.Fissures.Location;
import edu.iris.Fissures.LocationType;
import edu.iris.Fissures.Orientation;
import edu.iris.Fissures.Time;
import edu.iris.Fissures.TimeRange;
import edu.iris.Fissures.Unit;
import edu.iris.Fissures.IfNetwork.ChannelId;
import edu.iris.Fissures.IfNetwork.ChannelNotFound;
import edu.iris.Fissures.IfNetwork.Instrumentation;
import edu.iris.Fissures.IfNetwork.NetworkId;
import edu.iris.Fissures.IfNetwork.NetworkNotFound;
import edu.iris.Fissures.IfNetwork.SiteId;
import edu.iris.Fissures.IfNetwork.StationId;
import edu.iris.Fissures.model.QuantityImpl;
import edu.iris.Fissures.model.SamplingImpl;
import edu.iris.Fissures.model.TimeInterval;
import edu.iris.Fissures.model.UnitImpl;
import edu.iris.Fissures.network.ChannelImpl;
import edu.iris.Fissures.network.NetworkAttrImpl;
import edu.iris.Fissures.network.NetworkIdUtil;
import edu.iris.Fissures.network.SiteImpl;
import edu.iris.Fissures.network.StationIdUtil;
import edu.iris.Fissures.network.StationImpl;
import edu.sc.seis.fissuresUtil.cache.CacheNetworkAccess;
import edu.sc.seis.fissuresUtil.display.configuration.DOMHelper;
import edu.sc.seis.fissuresUtil.sac.InvalidResponse;
import edu.sc.seis.seisFile.fdsnws.stationxml.Channel;
import edu.sc.seis.sod.ConfigurationException;
import edu.sc.seis.sod.UserConfigurationException;
import edu.sc.seis.sod.source.AbstractCSVSource;
import edu.sc.seis.sod.subsetter.AreaSubsetter;

public class CSVNetworkSource extends AbstractCSVSource implements NetworkSource {

    public CSVNetworkSource(Element config) throws ConfigurationException {
        super(config, "CSVNetworkSource");
        initStations(config);
        initChannels(config);
    }
    
    protected void initStations(Element config) throws ConfigurationException {
        if (DOMHelper.hasElement(config, "stationFile")) {
            String filename = DOMHelper.extractText(config, "stationFile");
            this.csvFilename = filename;
            try {
                stations = getStationsFromReader(AreaSubsetter.makeRelativeOrRecipeDirReader(csvFilename));
            } catch(FileNotFoundException e) {
                throw new UserConfigurationException(e.getMessage() + " as a station CSV file.");
            } catch(IOException e) {
                throw new ConfigurationException("Unable to read " + csvFilename, e);
            }
        } else if (DOMHelper.hasElement(config, "stations")) {
            try {
                stations = getStationsFromReader(new StringReader(DOMHelper.extractText(config, "stations").trim()));
            } catch(IOException e) {
                throw new ConfigurationException("Unable to read stations from:"
                        + DOMHelper.extractText(config, "stations"), e);
            }
        } else {
            throw new ConfigurationException("Can't find stationFile or stations in configuration.");
        }
        networks = getNetworksFromStations(stations);
    }
    
    protected void initChannels(Element config) throws ConfigurationException {
        if (DOMHelper.hasElement(config, "channelFile")) {
            String filename = DOMHelper.extractText(config, "channelFile");
            try {
                channels = getChannelsFromReader(AreaSubsetter.makeRelativeOrRecipeDirReader(filename),
                                                 stations);
            } catch(FileNotFoundException e) {
                throw new UserConfigurationException(e.getMessage() + " as a channel CSV file.", e);
            } catch(IOException e) {
                throw new ConfigurationException("Unable to read " + filename, e);
            }
        } else if (DOMHelper.hasElement(config, "channels")) {
            try {
                channels = getChannelsFromReader(new StringReader(DOMHelper.extractText(config, "channels").trim()),
                                                 stations);
            } catch(IOException e) {
                throw new ConfigurationException("Unable to read channels from:"
                        + DOMHelper.extractText(config, "channels"), e);
            }
        } else {
            throw new ConfigurationException("Can't find channelFile or channels in configuration.");
        }
    }

    public CSVNetworkSource(String stationFile, String channelFile) throws ConfigurationException, FileNotFoundException, IOException {
        super("CSVNetworkSource");
        stations = getStationsFromReader(AreaSubsetter.makeRelativeOrRecipeDirReader(stationFile));
        channels = getChannelsFromReader(AreaSubsetter.makeRelativeOrRecipeDirReader(channelFile),
                                         stations);
    }

    public String getDescription() {
        return "CSVNetworkSource: " + csvFilename;
    }

    public List<NetworkAttrImpl> getNetworksFromStations(List<StationImpl> staList) {
        Map<String, NetworkAttrImpl> nets = new HashMap<String, NetworkAttrImpl>();
        for (StationImpl sta : staList) {
            nets.put(StationIdUtil.toStringNoDates(sta.getId()),
                     new NetworkAttrImpl(sta.getId().network_id, "", "", ""));
        }
        List<NetworkAttrImpl> out = new ArrayList<NetworkAttrImpl>();
        for (NetworkAttrImpl net : nets.values()) {
            out.add(net);
        }
        return out;
    }

    public List<StationImpl> getStationsFromReader(Reader reader) throws IOException, FileNotFoundException,
            ConfigurationException {
        List<StationImpl> stations = new ArrayList<StationImpl>();
        CsvReader csvReader = new CsvReader(reader);
        List<String> headers = validateHeaders(csvReader);
        while (csvReader.readRecord()) {
            // time to start populating field values
            // first up: the only required field...
            String netCode = csvReader.get(NET_CODE);
            String staCode = csvReader.get(CODE);
            float latitude = loadFloat(headers, csvReader, LATITUDE, 0);
            float longitude = loadFloat(headers, csvReader, LONGITUDE, 0);
            double elevation = loadDouble(headers, csvReader, ELEVATION, 0);
            double depth = loadDouble(headers, csvReader, DEPTH, 0);
            Unit elevationUnit = loadUnit(headers, csvReader, ELEVATION_UNITS, UnitImpl.METER);
            Unit depthUnit = loadUnit(headers, csvReader, DEPTH_UNITS, UnitImpl.METER);
            Location location = new Location(latitude,
                                             longitude,
                                             new QuantityImpl(elevation, elevationUnit),
                                             new QuantityImpl(depth, depthUnit),
                                             LocationType.GEOGRAPHIC);
            NetworkId netId = new NetworkId(netCode, loadTime(headers, csvReader, NET_START, DEFAULT_TIME));
            Time staBegin = loadTime(headers, csvReader, START, DEFAULT_TIME);
            StationId staId = new StationId(netId, staCode, staBegin);
            StationImpl station = new StationImpl(staId,
                                                  loadString(headers, csvReader, NAME, ""),
                                                  location,
                                                  loadString(headers, csvReader, OPERATOR, ""),
                                                  loadString(headers, csvReader, DESCRIPTION, ""),
                                                  loadString(headers, csvReader, COMMENT, ""),
                                                  new NetworkAttrImpl(netId, "", "", ""));
            stations.add(station);
        }
        return stations;
    }

    protected StationImpl getStationForChannel(String netCode, String staCode) {
        for (StationImpl stationImpl : stations) {
            if (netCode.equals(stationImpl.getNetworkAttrImpl().get_code())
                    && staCode.equals(stationImpl.get_code())) {
                return stationImpl;
            }
        }
        return null;
    }
    
    public List<ChannelImpl> getChannelsFromReader(Reader reader, List<StationImpl> stations) throws IOException,
            FileNotFoundException, ConfigurationException {
        List<ChannelImpl> channels = new ArrayList<ChannelImpl>();
        CsvReader csvReader = new CsvReader(reader);
        List<String> headers = validateHeaders(csvReader);
        while (csvReader.readRecord()) {
            String netCode = csvReader.get(NET_CODE);
            String staCode = csvReader.get(STATION_CODE);
            String siteCode = Channel.fixLocCode(csvReader.get(SITE_CODE));
            String chanCode = csvReader.get(CODE);
            StationImpl curStation = getStationForChannel(netCode, staCode);
            if (curStation == null) {
                throw new UserConfigurationException("Station " + netCode + "." + staCode
                        + " is not a known station. Add it to the stations section.");
            }
            Location location;
            if (headers.contains(LATITUDE) || headers.contains(LONGITUDE) || headers.contains(ELEVATION)
                    || headers.contains(DEPTH)) {
                float latitude = loadFloat(headers, csvReader, LATITUDE, 0);
                float longitude = loadFloat(headers, csvReader, LONGITUDE, 0);
                double elevation = loadDouble(headers, csvReader, ELEVATION, 0);
                double depth = loadDouble(headers, csvReader, DEPTH, 0);
                Unit elevationUnit = loadUnit(headers, csvReader, ELEVATION_UNITS, UnitImpl.METER);
                Unit depthUnit = loadUnit(headers, csvReader, DEPTH_UNITS, UnitImpl.METER);
                location = new Location(latitude,
                                        longitude,
                                        new QuantityImpl(elevation, elevationUnit),
                                        new QuantityImpl(depth, depthUnit),
                                        LocationType.GEOGRAPHIC);
            } else {
                location = curStation.getLocation();
            }
            Time chanBegin = loadTime(headers, csvReader, START, DEFAULT_TIME);
            float azimuth = loadFloat(headers, csvReader, AZIMUTH, ChannelImpl.getAzimuth(chanCode));
            float dip = loadFloat(headers, csvReader, DIP, ChannelImpl.getDip(chanCode));
            SamplingImpl sampling;
            if (headers.contains(SAMPLE_PERIOD)) {
                sampling = new SamplingImpl(1, new TimeInterval(loadFloat(headers, csvReader, SAMPLE_PERIOD, 1),
                                                                UnitImpl.SECOND));
            } else if (headers.contains(SAMPLE_PERIOD)) {
                sampling = new SamplingImpl(1, new TimeInterval(1 / loadFloat(headers, csvReader, SAMPLE_FREQUENCY, 1),
                                                                UnitImpl.SECOND));
            } else {
                sampling = new SamplingImpl(1, new TimeInterval(1, UnitImpl.SECOND));
            }
            TimeRange chanTime = new TimeRange(chanBegin, loadTime(headers, csvReader, END, DEFAULT_END));
            ChannelImpl channel = new ChannelImpl(new ChannelId(curStation.get_id().network_id,
                                                                staCode,
                                                                siteCode,
                                                                chanCode,
                                                                chanBegin),
                                                  loadString(headers, csvReader, NAME, ""),
                                                  new Orientation(azimuth, dip),
                                                  sampling,
                                                  chanTime,
                                                  new SiteImpl(new SiteId(curStation.get_id().network_id,
                                                                          staCode,
                                                                          siteCode,
                                                                          chanBegin), location, curStation, ""));
            channels.add(channel);
        }
        return channels;
    }

    @Override

    public void setConstraints(NetworkQueryConstraints constraints) {
        // no op
    }

    public String toString() {
        return "CSVNetworkSource using " + csvFilename;
    }

    protected List<NetworkAttrImpl> networks;

    protected List<StationImpl> stations;

    protected List<ChannelImpl> channels;

    // required
    public static final String NET_CODE = "net.code";

    public static final String STATION_CODE = "station.code";

    public static final String SITE_CODE = "site.code";

    public static final String CODE = "code";

    // optional
    public static final String NET_START = "net.start";

    public static final String NET_END = "net.end";

    public static final String SAMPLE_PERIOD = "sampling.period";

    public static final String SAMPLE_FREQUENCY = "sampling.frequency";

    public static final String START = "start";

    public static final String END = "end";

    public static final String OPERATOR = "operator";

    public static final String COMMENT = "comment";

    public static final String DESCRIPTION = "description";

    // defaultable
    public static final String AZIMUTH = "azimuth";

    public static final String DIP = "dip";

    @Override
    public List<? extends ChannelImpl> getChannels(StationImpl station) {
        List<ChannelImpl> out = new ArrayList<ChannelImpl>();
        for (ChannelImpl chan : channels) {
            if (StationIdUtil.areEqual(station.getId(), chan.getStationImpl().getId())) {
                out.add(chan);
            }
        }
        return out;
    }

    @Override
    public Instrumentation getInstrumentation(ChannelImpl chanId) throws ChannelNotFound, InvalidResponse {
        throw new ChannelNotFound();
    }

    @Override
    public CacheNetworkAccess getNetwork(NetworkAttrImpl attr) {
        return new CacheNetworkAccess(null, attr);
    }

    @Override
    public List<? extends CacheNetworkAccess> getNetworkByName(String name) throws NetworkNotFound {
        throw new NetworkNotFound();
    }

    @Override
    public List<? extends NetworkAttrImpl> getNetworks() {
        return Collections.unmodifiableList(networks);
    }

    @Override
    public QuantityImpl getSensitivity(ChannelImpl chanId) throws ChannelNotFound, InvalidResponse {
        throw new ChannelNotFound();
    }

    @Override
    public List<? extends StationImpl> getStations(NetworkAttrImpl net) {
        List<StationImpl> staList = new ArrayList<StationImpl>();
        for (StationImpl sta : stations) {
            if (NetworkIdUtil.areEqual(net.getId(), sta.getId().network_id)) {
                staList.add(sta);
            }
        }
        return staList;
    }

    public String[] getFields() {
        return networkFields;
    }

    private static final String[] networkFields = new String[] {NET_CODE,
                                                                STATION_CODE,
                                                                SITE_CODE,
                                                                CODE,
                                                                LONGITUDE,
                                                                LATITUDE,
                                                                ELEVATION,
                                                                ELEVATION_UNITS,
                                                                DEPTH,
                                                                DEPTH_UNITS,
                                                                SAMPLE_PERIOD,
                                                                SAMPLE_FREQUENCY,
                                                                NET_START,
                                                                NET_END,
                                                                START,
                                                                END,
                                                                NAME,
                                                                FE_SEIS_REGION,
                                                                FE_GEO_REGION,
                                                                FE_REGION,
                                                                FE_REGION_TYPE,
                                                                AZIMUTH,
                                                                DIP,
                                                                OPERATOR,
                                                                COMMENT,
                                                                DESCRIPTION};

    @Override
    public TimeInterval getRefreshInterval() {
        return new TimeInterval(0, UnitImpl.MILLISECOND);
    }

    @Override
    public String getName() {
        if (csvFilename != null && csvFilename.length() != 0) {
            return csvFilename;
        }
        return "inline";
    }

}