package com.wytehat.btlogger;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * CSV export for whatever the map is currently showing.
 *
 * Two flavours: RAW writes the history exactly as recorded, NORMALIZED runs
 * it through {@link HistoryNormalizer} first and also writes out the points
 * that were removed, so the file explains its own cleanup.
 *
 * Saving is a two-step dance with the system file picker - {@link #start}
 * asks for a destination, the activity hands the chosen Uri back to
 * {@link #write} from onActivityResult.
 */
public class MapExporter {

    public static final int REQUEST_EXPORT_RAW = 7401;
    public static final int REQUEST_EXPORT_NORMALIZED = 7402;

    private static final String HEADER_ROW =
        "Record Type,Device Name,Address,Category,"
            + "Manufacturer,Latitude,Longitude,Accuracy,"
            + "Battery,RSSI,Last Seen,Status,"
            + "Location Locked,Map Enabled,Show Trail,Photo URI";

    /** Snapshot of the map's filter bar at the moment of export. */
    public static class Filters {

        public String device = "All";
        public String category = "All";

        public long startMillis;
        public long endMillis;

        public boolean includeHistory;

        /** Same rule the map uses: an undated point is never filtered out. */
        public boolean contains(long time) {

            if (time <= 0L) return true;

            return time >= startMillis && time <= endMillis;
        }
    }

    private final Activity activity;
    private final TrackerDatabase db;
    private final HistoryNormalizer normalizer;

    public MapExporter(
            Activity activity,
            TrackerDatabase db,
            HistoryNormalizer normalizer) {

        this.activity = activity;
        this.db = db;
        this.normalizer = normalizer;
    }

    /**
     * Asks the system where to save. The caller passes the devices already
     * matching the filter bar, so the file picker is never opened for an
     * export that would come back empty.
     *
     * @return false if there was nothing to export, or no app to save with.
     */
    public boolean start(
            List<DeviceRecord> devices,
            boolean normalized,
            Filters filters) {

        if (devices == null || devices.size() == 0) {

            toast("No database records match the current filters.");

            return false;
        }

        if (normalized) {

            normalizer.clear();
            normalizer.prepare(db, devices);
        }

        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("text/csv");

        intent.putExtra(
            Intent.EXTRA_TITLE,
            buildFilename(filters, normalized));

        try {

            activity.startActivityForResult(
                intent,
                normalized
                    ? REQUEST_EXPORT_NORMALIZED
                    : REQUEST_EXPORT_RAW);

            return true;

        } catch (ActivityNotFoundException error) {

            toast("No file-saving application is available.");

            return false;
        }
    }

    /** Writes the export to the destination the user picked. */
    public void write(
            Uri uri,
            List<DeviceRecord> devices,
            boolean normalized,
            Filters filters) {

        OutputStream output = null;
        BufferedWriter writer = null;

        try {

            ContentResolver resolver = activity.getContentResolver();

            output = resolver.openOutputStream(uri);

            if (output == null) {
                throw new IOException("Unable to open output file.");
            }

            writer = new BufferedWriter(
                new OutputStreamWriter(output, "UTF-8"));

            writeHeader(writer, normalized, filters);

            int deviceCount = 0;
            int pointCount = 0;

            for (int i = 0; i < devices.size(); i++) {

                DeviceRecord record = devices.get(i);

                if (record == null) continue;

                writeDeviceRow(writer, record, normalized);

                deviceCount++;

                if (!filters.includeHistory) continue;

                if (record.address == null
                        || record.address.trim().length() == 0) {

                    continue;
                }

                pointCount += writeHistoryRows(
                    writer,
                    record,
                    normalized,
                    filters);

                if (normalized) {
                    writeRemovedRows(writer, record, filters);
                }
            }

            writeSummary(writer, normalized, deviceCount, pointCount);

            writer.flush();

            toast(summaryMessage(normalized, deviceCount, pointCount));

        } catch (Exception error) {

            toast("Export failed: " + error.getMessage());

        } finally {

            try {

                if (writer != null) writer.close();
                else if (output != null) output.close();

            } catch (Exception ignored) {
            }
        }
    }

    // ------------------------------------------------------------------
    // Sections
    // ------------------------------------------------------------------

    private void writeHeader(
            BufferedWriter writer,
            boolean normalized,
            Filters filters) throws IOException {

        line(writer, MapFormat.csv(
            normalized
                ? "BT Logger Normalized Database Export"
                : "BT Logger Raw Database Export"));

        pair(writer, "Exported", MapFormat.date(System.currentTimeMillis()));
        pair(writer, "Data Mode", normalized ? "Normalized" : "Raw");
        pair(writer, "Selected Device", filters.device);
        pair(writer, "Category", filters.category);
        pair(writer, "Start", MapFormat.date(filters.startMillis));
        pair(writer, "End", MapFormat.date(filters.endMillis));

        pair(
            writer,
            "Historical Points",
            filters.includeHistory ? "Included" : "Excluded");

        if (normalized) {

            pair(
                writer,
                "Normalization Aggressiveness",
                normalizer.getAggressiveness() + "%");

            pair(
                writer,
                "Maximum Speed",
                normalizer.maxSpeedKmh() + " km/h");

            pair(
                writer,
                "Duplicate Distance",
                normalizer.duplicateDistance() + " m");

            pair(
                writer,
                "Same Time Distance",
                normalizer.sameTimeDistance() + " m");

            pair(
                writer,
                "Accuracy Multiplier",
                Double.toString(normalizer.accuracyMultiplier()));
        }

        writer.newLine();

        line(writer, HEADER_ROW);
    }

    private void writeDeviceRow(
            BufferedWriter writer,
            DeviceRecord record,
            boolean normalized) throws IOException {

        double latitude = record.latitude;
        double longitude = record.longitude;

        long time = record.updatedAt;

        /*
         * In normalized mode the device's own last-known fix may be one of
         * the points that got thrown out, so report the newest surviving
         * point instead.
         */
        if (normalized) {

            List<LocationPoint> history =
                normalizer.historyOf(db, record.address);

            if (history.size() > 0) {

                LocationPoint latest = history.get(history.size() - 1);

                if (latest != null) {

                    latitude = latest.latitude;
                    longitude = latest.longitude;

                    long latestTime = HistoryNormalizer.pointTime(latest);

                    if (latestTime > 0) time = latestTime;
                }
            }
        }

        line(writer,
            MapFormat.csv("DEVICE") + ","
                + MapFormat.csv(record.displayName()) + ","
                + MapFormat.csv(record.address) + ","
                + MapFormat.csv(record.category) + ","
                + MapFormat.csv(record.vendorName) + ","
                + MapFormat.csv(Double.toString(latitude)) + ","
                + MapFormat.csv(Double.toString(longitude)) + ","
                + MapFormat.csv(
                    record.accuracy > 0
                        ? Double.toString(record.accuracy)
                        : "") + ","
                + MapFormat.csv(
                    record.battery >= 0
                        ? Integer.toString(record.battery)
                        : "") + ","
                + MapFormat.csv(
                    record.rssi != 0
                        ? Integer.toString(record.rssi)
                        : "") + ","
                + MapFormat.csv(time > 0 ? MapFormat.date(time) : "") + ","
                + MapFormat.csv(MapFormat.reason(record.reason)) + ","
                + MapFormat.csv(Integer.toString(record.locationLocked)) + ","
                + MapFormat.csv(Integer.toString(record.mapEnabled)) + ","
                + MapFormat.csv(Integer.toString(record.showTrail)) + ","
                + MapFormat.csv(record.photoUri));
    }

    private int writeHistoryRows(
            BufferedWriter writer,
            DeviceRecord record,
            boolean normalized,
            Filters filters) throws IOException {

        List<LocationPoint> history =
            normalized
                ? normalizer.historyOf(db, record.address)
                : db.history(record.address, 0L);

        int written = 0;

        for (int p = 0; p < history.size(); p++) {

            LocationPoint point = history.get(p);

            if (point == null) continue;

            long time = HistoryNormalizer.pointTime(point);

            if (time > 0L && !filters.contains(time)) continue;

            double accuracy =
                HistoryNormalizer.pointDouble(point, "accuracy", -1);

            double rssi = HistoryNormalizer.pointDouble(point, "rssi", 0);

            double battery =
                HistoryNormalizer.pointDouble(point, "battery", -1);

            line(writer,
                MapFormat.csv(
                    normalized
                        ? "NORMALIZED_POINT"
                        : "HISTORICAL_POINT") + ","
                    + MapFormat.csv(record.displayName()) + ","
                    + MapFormat.csv(record.address) + ","
                    + MapFormat.csv(record.category) + ","
                    + MapFormat.csv(record.vendorName) + ","
                    + MapFormat.csv(Double.toString(point.latitude)) + ","
                    + MapFormat.csv(Double.toString(point.longitude)) + ","
                    + MapFormat.csv(
                        accuracy > 0 ? Double.toString(accuracy) : "") + ","
                    + MapFormat.csv(
                        battery >= 0 ? Double.toString(battery) : "") + ","
                    + MapFormat.csv(
                        rssi != 0 ? Double.toString(rssi) : "") + ","
                    + MapFormat.csv(time > 0 ? MapFormat.date(time) : "") + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv(""));

            written++;
        }

        return written;
    }

    /** The points normalization threw away, with the reason in the Status column. */
    private void writeRemovedRows(
            BufferedWriter writer,
            DeviceRecord record,
            Filters filters) throws IOException {

        List<HistoryNormalizer.Decision> decisions =
            normalizer.decisionsOf(record.address);

        for (HistoryNormalizer.Decision decision : decisions) {

            if (decision == null || decision.point == null) continue;

            long time = HistoryNormalizer.pointTime(decision.point);

            if (time > 0 && !filters.contains(time)) continue;

            line(writer,
                MapFormat.csv("REMOVED_POINT") + ","
                    + MapFormat.csv(record.displayName()) + ","
                    + MapFormat.csv(record.address) + ","
                    + MapFormat.csv(record.category) + ","
                    + MapFormat.csv(record.vendorName) + ","
                    + MapFormat.csv(
                        Double.toString(decision.point.latitude)) + ","
                    + MapFormat.csv(
                        Double.toString(decision.point.longitude)) + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv(time > 0 ? MapFormat.date(time) : "") + ","
                    + MapFormat.csv(decision.reason) + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv("") + ","
                    + MapFormat.csv(""));
        }
    }

    private void writeSummary(
            BufferedWriter writer,
            boolean normalized,
            int deviceCount,
            int pointCount) throws IOException {

        writer.newLine();

        line(writer, MapFormat.csv("Export Summary"));

        pair(writer, "Devices Exported", Integer.toString(deviceCount));

        pair(
            writer,
            normalized
                ? "Normalized Points Exported"
                : "Historical Points Exported",
            Integer.toString(pointCount));

        if (normalized) {

            pair(
                writer,
                "Duplicates Removed",
                Integer.toString(normalizer.duplicateCount()));

            pair(
                writer,
                "GPS Jumps Rejected",
                Integer.toString(normalizer.rejectedCount()));
        }
    }

    private String summaryMessage(
            boolean normalized,
            int deviceCount,
            int pointCount) {

        if (!normalized) {

            return "Exported "
                + deviceCount + " device(s) and "
                + pointCount + " historical point(s).";
        }

        return "Exported "
            + deviceCount + " device(s), "
            + pointCount + " normalized point(s), "
            + normalizer.duplicateCount() + " duplicate(s) removed, and "
            + normalizer.rejectedCount() + " GPS jump(s) rejected.";
    }

    // ------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------

    private String buildFilename(Filters filters, boolean normalized) {

        String devicePart = sanitizeFilename(
            "All".equals(filters.device)
                ? "all_devices"
                : filters.device);

        String datePart =
            new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
                .format(new Date());

        return "btlogger_"
            + devicePart
            + "_"
            + (normalized ? "normalized_" : "raw_")
            + datePart
            + ".csv";
    }

    private String sanitizeFilename(String value) {

        if (value == null || value.trim().length() == 0) {
            return "export";
        }

        return value
            .replaceAll("[\\\\/:*?\"<>|]", "_")
            .replaceAll("\\s+", "_");
    }

    private void line(BufferedWriter writer, String text) throws IOException {

        writer.write(text);
        writer.newLine();
    }

    private void pair(
            BufferedWriter writer,
            String label,
            String value) throws IOException {

        line(writer, MapFormat.csv(label) + "," + MapFormat.csv(value));
    }

    private void toast(String message) {

        Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
    }
}
