package btools.routingapp;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Random;

import btools.mapaccess.PhysicalFile;
import btools.mapaccess.Rd5DiffManager;
import btools.mapaccess.Rd5DiffTool;
import btools.util.ProgressListener;

public class DownloadWorker extends Worker {
  public static final String KEY_INPUT_SEGMENT_NAMES = "SEGMENT_NAMES";
  public static final String KEY_OUTPUT_ERROR = "ERROR";
  public static final String PROGRESS_SEGMENT_NAME = "PROGRESS_SEGMENT_NAME";
  public static final String PROGRESS_SEGMENT_PERCENT = "PROGRESS_SEGMENT_PERCENT";

  private final static boolean DEBUG = false;
  private static final int NOTIFICATION_ID = new Random().nextInt();
  private static final String PROFILES_DIR = "profiles2/";
  private static final String SEGMENTS_DIR = "segments4/";
  private static final String SEGMENT_DIFF_SUFFIX = ".df5";
  private static final String SEGMENT_SUFFIX = ".rd5";
  private static final String LOG_TAG = "DownloadWorker";

  private final NotificationManager notificationManager;
  private final ServerConfig mServerConfig;
  private final File baseDir;
  private final ProgressListener diffProgressListener;
  private final DownloadProgressListener downloadProgressListener;
  private final Data.Builder progressBuilder = new Data.Builder();
  private final NotificationCompat.Builder notificationBuilder;

  public DownloadWorker(
    @NonNull Context context,
    @NonNull WorkerParameters parameters) {
    super(context, parameters);
    notificationManager = (NotificationManager)
      context.getSystemService(Context.NOTIFICATION_SERVICE);
    mServerConfig = new ServerConfig(context);
    baseDir = new File(ConfigHelper.getBaseDir(context), "brouter");

    notificationBuilder = createNotificationBuilder();

    downloadProgressListener = new DownloadProgressListener() {
      private String currentDownloadName;
      private DownloadType currentDownloadType;
      private int lastProgressPercent;

      @Override
      public void onDownloadStart(String downloadName, DownloadType downloadType) {
        currentDownloadName = downloadName;
        currentDownloadType = downloadType;
        if (downloadType == DownloadType.SEGMENT) {
          progressBuilder.putString(PROGRESS_SEGMENT_NAME, downloadName);
          notificationBuilder.setContentText(downloadName);
        }
      }

      @Override
      public void onDownloadInfo(String info) {
        notificationBuilder.setContentText(currentDownloadName + ": " + info);
        notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
      }

      @Override
      public void onDownloadProgress(int max, int progress) {
        int progressPercent = (int) (progress * 100L / max);

        // Only report segments and update if it changed to avoid hammering NotificationManager
        if (currentDownloadType != DownloadType.SEGMENT || progressPercent == lastProgressPercent) {
          return;
        }

        if (max > 0) {
          notificationBuilder.setProgress(max, progress, false);
          progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, progressPercent);
        } else {
          notificationBuilder.setProgress(0, 0, true);
          progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, -1);
        }
        notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
        setProgressAsync(progressBuilder.build());

        lastProgressPercent = progressPercent;
      }

      @Override
      public void onDownloadFinished() {
      }
    };

    diffProgressListener = new ProgressListener() {
      @Override
      public void updateProgress(String task, int progress) {
        downloadProgressListener.onDownloadInfo(task);
        downloadProgressListener.onDownloadProgress(100, progress);
      }

      @Override
      public boolean isCanceled() {
        return isStopped();
      }
    };
  }

  @NonNull
  @Override
  public Result doWork() {
    Data inputData = getInputData();
    Data.Builder output = new Data.Builder();
    String[] segmentNames = inputData.getStringArray(KEY_INPUT_SEGMENT_NAMES);
    if (segmentNames == null) {
      if (DEBUG) Log.d(LOG_TAG, "Failure: no segmentNames");
      return Result.failure();
    }
    notificationBuilder.setContentText("Starting Download");
    // Mark the Worker as important
    setForegroundAsync(new ForegroundInfo(NOTIFICATION_ID, notificationBuilder.build()));
    try {
      if (DEBUG) Log.d(LOG_TAG, "Download lookup & profiles");
      downloadLookupAndProfiles();

      for (String segmentName : segmentNames) {
        downloadProgressListener.onDownloadStart(segmentName, DownloadType.SEGMENT);
        if (DEBUG) Log.d(LOG_TAG, "Download segment " + segmentName);
        downloadSegment(mServerConfig.getSegmentUrl(), segmentName + SEGMENT_SUFFIX);
      }
    } catch (IOException e) {
      Log.w(LOG_TAG, e);
      output.putString(KEY_OUTPUT_ERROR, e.toString());
      return Result.failure(output.build());
    } catch (InterruptedException e) {
      Log.w(LOG_TAG, e);
      output.putString(KEY_OUTPUT_ERROR, e.toString());
      return Result.failure(output.build());
    }
    if (DEBUG) Log.d(LOG_TAG, "doWork finished");
    return Result.success();
  }

  private void downloadLookupAndProfiles() throws IOException, InterruptedException {
    String[] lookups = mServerConfig.getLookups();
    for (String fileName : lookups) {
      if (fileName.length() > 0) {
        File lookupFile = new File(baseDir, PROFILES_DIR + fileName);
        String lookupLocation = mServerConfig.getLookupUrl() + fileName;
        URL lookupUrl = new URL(lookupLocation);
        downloadProgressListener.onDownloadStart(fileName, DownloadType.LOOKUP);
        downloadFile(lookupUrl, lookupFile, false);
        downloadProgressListener.onDownloadFinished();
      }
    }

    String[] profiles = mServerConfig.getProfiles();
    for (String fileName : profiles) {
      if (fileName.length() > 0) {
        File profileFile = new File(baseDir, PROFILES_DIR + fileName);
        if (profileFile.exists()) {
          String profileLocation = mServerConfig.getProfilesUrl() + fileName;
          URL profileUrl = new URL(profileLocation);
          downloadProgressListener.onDownloadStart(fileName, DownloadType.PROFILE);
          downloadFile(profileUrl, profileFile, false);
          downloadProgressListener.onDownloadFinished();
        }
      }
    }
  }

  private void downloadSegment(String segmentBaseUrl, String segmentName) throws IOException, InterruptedException {
    File segmentFile = new File(baseDir, SEGMENTS_DIR + segmentName);
    File segmentFileTemp = new File(segmentFile.getAbsolutePath() + "_tmp");
    try {
      if (segmentFile.exists()) {
        if (DEBUG) Log.d(LOG_TAG, "Calculating local checksum");
        String md5 = Rd5DiffManager.getMD5(segmentFile);
        String segmentDeltaLocation = segmentBaseUrl + "diff/" + segmentName.replace(SEGMENT_SUFFIX, "/" + md5 + SEGMENT_DIFF_SUFFIX);
        URL segmentDeltaUrl = new URL(segmentDeltaLocation);
        if (httpFileExists(segmentDeltaUrl)) {
          File segmentDeltaFile = new File(segmentFile.getAbsolutePath() + "_diff");
          try {
            downloadFile(segmentDeltaUrl, segmentDeltaFile, true);
            if (DEBUG) Log.d(LOG_TAG, "Applying delta");
            Rd5DiffTool.recoverFromDelta(segmentFile, segmentDeltaFile, segmentFileTemp, diffProgressListener);
          } catch (IOException e) {
            throw new IOException("Failed to download & apply delta update", e);
          } finally {
            segmentDeltaFile.delete();
          }
        }
      }

      if (!segmentFileTemp.exists()) {
        URL segmentUrl = new URL(segmentBaseUrl + segmentName);
        downloadFile(segmentUrl, segmentFileTemp, true);
      }

      PhysicalFile.checkFileIntegrity(segmentFileTemp);
      if (segmentFile.exists()) {
        if (!segmentFile.delete()) {
          throw new IOException("Failed to delete existing " + segmentFile.getAbsolutePath());
        }
      }

      if (!segmentFileTemp.renameTo(segmentFile)) {
        throw new IOException("Failed to write " + segmentFile.getAbsolutePath());
      }
    } finally {
      segmentFileTemp.delete();
    }
  }

  private boolean httpFileExists(URL downloadUrl) throws IOException {
    HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
    connection.setConnectTimeout(5000);
    connection.setRequestMethod("HEAD");
    connection.connect();

    return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
  }

  private void downloadFile(URL downloadUrl, File outputFile, boolean limitDownloadSpeed) throws IOException, InterruptedException {
    HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
    connection.setConnectTimeout(5000);
    connection.connect();

    if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
      throw new IOException("HTTP Request failed: " + downloadUrl + " returned " + connection.getResponseCode());
    }
    int fileLength = connection.getContentLength();
    try (
      InputStream input = connection.getInputStream();
      OutputStream output = new FileOutputStream(outputFile)
    ) {
      byte[] buffer = new byte[4096];
      int total = 0;
      long t0 = System.currentTimeMillis();
      int count;
      while ((count = input.read(buffer)) != -1) {
        if (isStopped()) {
          throw new InterruptedException();
        }
        total += count;
        output.write(buffer, 0, count);

        downloadProgressListener.onDownloadProgress(fileLength, total);

        if (limitDownloadSpeed) {
          // enforce < 16 Mbit/s
          long dt = t0 + total / 2096 - System.currentTimeMillis();
          if (dt > 0) {
            Thread.sleep(dt);
          }
        }
      }
    }
  }

  @NonNull
  private NotificationCompat.Builder createNotificationBuilder() {
    Context context = getApplicationContext();
    String id = context.getString(R.string.notification_channel_id);
    String title = context.getString(R.string.notification_title);
    String cancel = context.getString(R.string.cancel_download);
    // This PendingIntent can be used to cancel the worker
    PendingIntent intent = WorkManager.getInstance(context)
      .createCancelPendingIntent(getId());

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      createChannel();
    }

    return new NotificationCompat.Builder(context, id)
      .setContentTitle(title)
      .setTicker(title)
      .setOnlyAlertOnce(true)
      .setPriority(NotificationCompat.PRIORITY_LOW)
      .setSmallIcon(android.R.drawable.stat_sys_download)
      .setOngoing(true)
      // Add the cancel action to the notification which can
      // be used to cancel the worker
      .addAction(android.R.drawable.ic_delete, cancel, intent);
  }

  @RequiresApi(Build.VERSION_CODES.O)
  private void createChannel() {
    CharSequence name = getApplicationContext().getString(R.string.channel_name);
    int importance = NotificationManager.IMPORTANCE_LOW;
    NotificationChannel channel = new NotificationChannel(getApplicationContext().getString(R.string.notification_channel_id), name, importance);
    // Register the channel with the system; you can't change the importance
    // or other notification behaviors after this
    notificationManager.createNotificationChannel(channel);
  }

  enum DownloadType {
    LOOKUP,
    PROFILE,
    SEGMENT
  }

  interface DownloadProgressListener {
    void onDownloadStart(String downloadName, DownloadType downloadType);

    void onDownloadInfo(String info);

    void onDownloadProgress(int max, int progress);

    void onDownloadFinished();
  }
}
