package org.thoughtcrime.securesms.connect;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.widget.Toast;

import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEventCenter;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;

import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientProvider;
import org.thoughtcrime.securesms.util.Hash;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class ApplicationDcContext extends DcContext {

  public static final String TAG = "DeltaChat";

  @IntDef({RECIPIENT_TYPE_CHAT, RECIPIENT_TYPE_CONTACT})
  public @interface RecipientType {
  }

  public static final int RECIPIENT_TYPE_CHAT = 0;
  public static final int RECIPIENT_TYPE_CONTACT = 1;

  public Context context;
  public volatile boolean isScreenOn = false;

  public ApplicationDcContext(Context context) {
    super("android-dev");
    this.context = context;

    File dbfile = new File(context.getFilesDir(), "messenger.db");
    open(dbfile.getAbsolutePath());
    setConfig("e2ee_enabled", "1");

    try {
      PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

      imapWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "imapWakeLock");
      imapWakeLock.setReferenceCounted(false); // if the idle-thread is killed for any reasons, it is better not to rely on reference counting

      smtpWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "smtpWakeLock");
      smtpWakeLock.setReferenceCounted(false); // if the idle-thread is killed for any reasons, it is better not to rely on reference counting

      afterForgroundWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "afterForegroundWakeLock");
      afterForgroundWakeLock.setReferenceCounted(false);

    } catch (Exception e) {
      Log.e(TAG, "Cannot create wakeLocks");
    }

    new ForegroundDetector(ApplicationContext.getInstance(context));
    startThreads(0);

    TimerReceiver.scheduleNextAlarm(context);

    BroadcastReceiver networkStateReceiver = new NetworkStateReceiver();
    context.registerReceiver(networkStateReceiver, new IntentFilter(android.net.ConnectivityManager.CONNECTIVITY_ACTION));

    IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
    filter.addAction(Intent.ACTION_SCREEN_OFF);
    BroadcastReceiver screenReceiver = new ScreenReceiver();
    context.registerReceiver(screenReceiver, filter);

    try {
      PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
      isScreenOn = pm.isScreenOn();
    } catch (Exception e) {

    }

    if (!isScreenOn) {
      context.startService(new Intent(context, KeepAliveService.class));
    }
  }

  public File getImexDir() {
    // DIRECTORY_DOCUMENTS is only available since KitKat;
    // as we also support Ice Cream Sandwich and Jellybean (2017: 11% in total), this is no option.
    // Moreover, DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
    // eg. "Download Managers" are nearly always installed.
    // CAVE: do not use DownloadManager to add the file as it is deleted on uninstall then ...
    return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
  }

  public static HashMap<String, Integer> sharedFiles = new HashMap<>();

  public void openForViewOrShare(int msg_id, String cmd) {
    DcMsg msg = getMsg(msg_id);
    String path = msg.getFile();
    String mimeType = msg.getFilemime();
    try {
      File file = new File(path);
      if (!file.exists()) {
        Toast.makeText(context, context.getString(R.string.ShareActivity_file_not_found, path), Toast.LENGTH_LONG).show();
        return;
      }

      Uri uri;
      if (path.startsWith(getBlobdir())) {
        uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + file.getName());
        sharedFiles.put("/" + file.getName(), 1); // as different Android version handle uris in putExtra differently, we also check them on our own
      } else {
        if (Build.VERSION.SDK_INT >= 24) {
          uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
        } else {
          uri = Uri.fromFile(file);
        }
      }

      if (cmd.equals(Intent.ACTION_VIEW)) {
        mimeType = checkMime(path, mimeType);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, mimeType);
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        context.startActivity(intent);
      } else {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType(mimeType);
        intent.putExtra(Intent.EXTRA_STREAM, uri);
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        context.startActivity(Intent.createChooser(intent, context.getString(R.string.ShareActivity_share_with)));
      }
    } catch (RuntimeException e) {
      Toast.makeText(context, R.string.ShareActivity_unable_to_open_media, Toast.LENGTH_LONG).show();
      Toast.makeText(context, "Media-Type: " + mimeType, Toast.LENGTH_LONG).show();
      Log.i(TAG, "opening of external activity failed.", e);
    }
  }

  private String checkMime(String path, String mimeType) {
    if(mimeType == null || mimeType.equals("application/octet-stream")) {
      String extension = MimeTypeMap.getFileExtensionFromUrl(path);
      String newType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
      if(newType != null) return newType;
    }
    return mimeType;
  }

  /***********************************************************************************************
   * create objects compatible to the database model of Signal
   **********************************************************************************************/

  @NonNull
  public Recipient getRecipient(@RecipientType int recipientType, int id) {
    switch (recipientType) {
      case RECIPIENT_TYPE_CHAT:
        return getRecipient(getChat(id));
      case RECIPIENT_TYPE_CONTACT:
        return getRecipient(getContact(id));
      default:
        throw new IllegalArgumentException("Wrong RecipientType");
    }
  }

  @NonNull
  public Recipient getRecipient(DcChat chat) {
    int[] contactIds = getChatContacts(chat.getId());
    List<Recipient> participants = new ArrayList<>();
    for (int contactId : contactIds) {
      participants.add(getRecipient(RECIPIENT_TYPE_CONTACT, contactId));
    }
    RecipientProvider.RecipientDetails recipientDetails = new RecipientProvider.RecipientDetails(chat.getName(), null, false, null, participants);
    Recipient recipient = new Recipient(Address.fromChat(chat.getId()), recipientDetails);
    if (!chat.isGroup()) {
      String identifier = Hash.sha256(chat.getName() + chat.getSubtitle());
      Uri systemContactPhoto = Prefs.getSystemContactPhoto(context, identifier);
      if (systemContactPhoto != null) {
        recipient.setSystemContactPhoto(systemContactPhoto);
      }
    }
    return recipient;
  }

  @NonNull

  public Recipient getRecipient(DcContact contact) {
    RecipientProvider.RecipientDetails recipientDetails = new RecipientProvider.RecipientDetails(contact.getDisplayName(), null, false, null, null);
    Recipient recipient = new Recipient(Address.fromContact(contact.getId()), recipientDetails);
    String identifier = Hash.sha256(contact.getName() + contact.getAddr());
    Uri systemContactPhoto = Prefs.getSystemContactPhoto(context, identifier);
    if (systemContactPhoto != null) {
      recipient.setSystemContactPhoto(systemContactPhoto);
    }
    if (contact.getId() == DcContact.DC_CONTACT_ID_SELF) {
      recipient.setProfileAvatar("SELF");
    }
    int[] blockedContacts = getBlockedContacts();
    boolean blocked = false;
    for (int blockedContact : blockedContacts) {
      if (contact.getId() == blockedContact) {
        blocked = true;
        break;
      }
    }
    recipient.setBlocked(blocked);
    return recipient;
  }

  @NonNull
  public ThreadRecord getThreadRecord(DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
    int chatId = chat.getId();
    int distributionType = chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK ? ThreadRecord.DistributionTypes.ARCHIVE : ThreadRecord.DistributionTypes.CONVERSATION;

    String body = summary.getText1();
    if (!body.isEmpty()) {
      body += ": ";
    }
    body += summary.getText2();

    Recipient recipient = getRecipient(chat);
    long date = summary.getTimestamp();
    long count = 1;
    int unreadCount = getFreshMsgCount(chatId);
    long type = 0;//cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
    boolean archived = chat.getArchived() != 0;
    int status = 0;
    long expiresIn = 0;
    long lastSeen = 0;
    boolean verified = chat.isVerified();

    return new ThreadRecord(context, body, null, recipient, date, count,
        unreadCount, chatId, 0, status, type,
        distributionType, archived, expiresIn, lastSeen, 0, verified, summary);
  }


  /***********************************************************************************************
   * Working Threads
   **********************************************************************************************/

  private final Object threadsCritical = new Object();

  private boolean imapThreadStartedVal;
  private final Object imapThreadStartedCond = new Object();
  public Thread imapThread = null;
  private PowerManager.WakeLock imapWakeLock = null;

  private boolean smtpThreadStartedVal;
  private final Object smtpThreadStartedCond = new Object();
  public Thread smtpThread = null;
  private PowerManager.WakeLock smtpWakeLock = null;

  public PowerManager.WakeLock afterForgroundWakeLock = null;

  public final static int INTERRUPT_IDLE = 0x01; // interrupt idle if the thread is already running

  public void startThreads(int flags) {
    synchronized (threadsCritical) {

      if (imapThread == null || !imapThread.isAlive()) {

        synchronized (imapThreadStartedCond) {
          imapThreadStartedVal = false;
        }

        imapThread = new Thread(() -> {
          // raise the starting condition
          // after acquiring a wakelock so that the process is not terminated.
          // as imapWakeLock is not reference counted that would result in a wakelock-gap is not needed here.
          imapWakeLock.acquire();
          synchronized (imapThreadStartedCond) {
            imapThreadStartedVal = true;
            imapThreadStartedCond.notifyAll();
          }

          Log.i(TAG, "###################### IMAP-Thread started. ######################");


          while (true) {
            imapWakeLock.acquire();
            performImapJobs();
            performImapFetch();
            imapWakeLock.release();
            performImapIdle();
          }
        }, "imapThread");
        imapThread.setPriority(Thread.NORM_PRIORITY);
        imapThread.start();
      } else {
        if ((flags & INTERRUPT_IDLE) != 0) {
          interruptImapIdle();
        }
      }

      if (smtpThread == null || !smtpThread.isAlive()) {

        synchronized (smtpThreadStartedCond) {
          smtpThreadStartedVal = false;
        }

        smtpThread = new Thread(() -> {
          smtpWakeLock.acquire();
          synchronized (smtpThreadStartedCond) {
            smtpThreadStartedVal = true;
            smtpThreadStartedCond.notifyAll();
          }

          Log.i(TAG, "###################### SMTP-Thread started. ######################");


          while (true) {
            smtpWakeLock.acquire();
            performSmtpJobs();
            smtpWakeLock.release();
            performSmtpIdle();
          }
        }, "smtpThread");
        smtpThread.setPriority(Thread.MAX_PRIORITY);
        smtpThread.start();
      }
    }
  }

  public void waitForThreadsRunning() {
    try {
      synchronized (imapThreadStartedCond) {
        while (!imapThreadStartedVal) {
          imapThreadStartedCond.wait();
        }
      }

      synchronized (smtpThreadStartedCond) {
        while (!smtpThreadStartedVal) {
          smtpThreadStartedCond.wait();
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }


  /***********************************************************************************************
   * Tools
   **********************************************************************************************/

  public boolean isNetworkConnected() {
    try {
      ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
      NetworkInfo netInfo = cm.getActiveNetworkInfo();
      if (netInfo != null && netInfo.isConnected()) {
        return true;
      }

    } catch (Exception e) {
    }
    return false;
  }

  /***********************************************************************************************
   * Event Handling
   **********************************************************************************************/

  public DcEventCenter eventCenter = new DcEventCenter();

  private final Object lastErrorLock = new Object();
  private String lastErrorString = "";
  private boolean showNextErrorAsToast = true;

  public void captureNextError() {
    synchronized (lastErrorLock) {
      showNextErrorAsToast = false;
      lastErrorString = "";
    }
  }

  public boolean hasCapturedError() {
    synchronized (lastErrorLock) {
      return !lastErrorString.isEmpty();
    }
  }

  public String getCapturedError() {
    synchronized (lastErrorLock) {
      return lastErrorString;
    }
  }

  public void endCaptureNextError() {
    synchronized (lastErrorLock) {
      showNextErrorAsToast = true;
    }
  }

  private void handleError(int event, boolean popUp, String string) {
    // log error
    boolean showAsToast;
    Log.e(TAG, string);
    synchronized (lastErrorLock) {
      lastErrorString = string;
      showAsToast = showNextErrorAsToast;
      showNextErrorAsToast = true;
    }

    // show error to user
    Util.runOnMain(() -> {
      if (popUp && showAsToast) {
        String toastString = string;

        if (event == DC_EVENT_ERROR_NETWORK) {
          if (!isNetworkConnected()) {
            toastString = context.getString(R.string.error_no_network);
          }
        }

        if (ForegroundDetector.getInstance().isForeground()) {
          Toast.makeText(context, toastString, Toast.LENGTH_LONG).show();
        }
      }
    });
  }

  @Override
  public long handleEvent(final int event, long data1, long data2) {
    switch (event) {
      case DC_EVENT_INFO:
        Log.i(TAG, dataToString(data2));
        break;

      case DC_EVENT_WARNING:
        Log.w(TAG, dataToString(data2));
        break;

      case DC_EVENT_ERROR:
        handleError(event, true, dataToString(data2));
        break;

      case DC_EVENT_ERROR_NETWORK:
        handleError(event, data1 != 0, dataToString(data2));
        break;

      case DC_EVENT_HTTP_GET:
        String httpContent = null;
        try {
          URL url = new URL(dataToString(data1));
          HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
          try {
            urlConnection.setConnectTimeout(10 * 1000);
            InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());

            BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));

            StringBuilder total = new StringBuilder();
            String line;
            while ((line = r.readLine()) != null) {
              total.append(line).append('\n');
            }
            httpContent = total.toString();
          } finally {
            urlConnection.disconnect();
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
        return stringToData(httpContent);

      case DC_EVENT_GET_STRING:
        String s;
        switch ((int) data1) { // the integers are defined in the core and used only here, an enum or sth. like that won't have a big benefit
          case 3:
            s = context.getString(R.string.summary_draft);
            break;
          case 8:
            s = context.getString(R.string.menu_deaddrop);
            break;
          case 13:
            s = context.getString(R.string.default_status_text);
            break;
          case 42:
            s = context.getString(R.string.autocrypt__asm_subject);
            break;
          case 43:
            s = context.getString(R.string.autocrypt__asm_general_body);
            break;
          default:
            s = null;
            break;
        }
        return stringToData(s);

      default: {
        final Object data1obj = data1IsString(event) ? dataToString(data1) : data1;
        final Object data2obj = data2IsString(event) ? dataToString(data2) : data2;
        if (eventCenter != null) {
          eventCenter.sendToObservers(event, data1obj, data2obj);
        }
      }
      break;
    }
    return 0;
  }
}
