package com.github.shadowsocks.plugin

import java.io.{File, FileNotFoundException, FileOutputStream, IOException}

import android.content.pm.{PackageManager, Signature}
import android.content.{BroadcastReceiver, ContentResolver, Intent}
import android.net.Uri
import android.os.Bundle
import android.util.{Base64, Log}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.CloseUtils.autoClose
import com.github.shadowsocks.utils.{Commandline, IOUtils}
import eu.chainfire.libsuperuser.Shell

import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer

/**
  * @author Mygod
  */
object PluginManager {
  /**
    * Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
    * to publish or has published plugins for this app. You can obtain your public key by executing:
    *
    * $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
    *
    * If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
    * public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
    */
  lazy val trustedSignatures: Set[Signature] =
    app.getPackageManager.getPackageInfo(app.getPackageName, PackageManager.GET_SIGNATURES).signatures.toSet +
    new Signature(Base64.decode(  // @Mygod
      """
        |MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
        |TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
        |ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
        |OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
        |b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
        |b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
        |AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
        |L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
        |Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
        |M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
        |YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
        |agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
        |6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
        |YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
        |zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
        |RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
        |wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
        |1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
      """, Base64.DEFAULT)) +
    new Signature(Base64.decode( // @madeye
      """
        |MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
        |CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
        |BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
        |ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
        |BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
        |9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
        |4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
        |AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
        |Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
        |AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
        |1iNuLIssdhDz3IL9Yg==
      """, Base64.DEFAULT))

  private var receiver: BroadcastReceiver = _
  private var cachedPlugins: Map[String, Plugin] = _
  def fetchPlugins(): Map[String, Plugin] = {
    if (receiver == null) receiver = app.listenForPackageChanges(synchronized(cachedPlugins = null))
    synchronized(if (cachedPlugins == null) {
      val pm = app.getPackageManager
      cachedPlugins = (
        pm.queryIntentContentProviders(new Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
          .map(new NativePlugin(_, pm)) :+
        NoPlugin
      ).map(plugin => plugin.id -> plugin).toMap
    })
    cachedPlugins
  }

  private def buildUri(id: String) = new Uri.Builder()
    .scheme(PluginContract.SCHEME)
    .authority(PluginContract.AUTHORITY)
    .path('/' + id)
    .build()
  def buildIntent(id: String, action: String): Intent = new Intent(action, buildUri(id))

  // the following parts are meant to be used by :bg
  @throws[Throwable]
  def init(options: PluginOptions): String = {
    if (options.id.isEmpty) return null
    var throwable: Throwable = null

    try initNative(options) match {
      case null =>
      case path => return path
    } catch {
      case t: Throwable =>
        t.printStackTrace()
        if (throwable == null) throwable = t
    }

    // add other plugin types here

    throw if (throwable == null) new FileNotFoundException(
      app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) else throwable
  }

  @throws[IOException]
  @throws[IndexOutOfBoundsException]
  @throws[AssertionError]
  private def initNative(options: PluginOptions): String = {
    val providers = app.getPackageManager.queryIntentContentProviders(
      new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
    assert(providers.length == 1)
    val uri = new Uri.Builder()
      .scheme(ContentResolver.SCHEME_CONTENT)
      .authority(providers(0).providerInfo.authority)
      .build()
    val cr = app.getContentResolver
    try initNativeFast(cr, options, uri) catch {
      case t: Throwable =>
        t.printStackTrace()
        Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
        initNativeSlow(cr, options, uri)
    }
  }

  private def initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String = {
    val out = new Bundle()
    out.putString(PluginContract.EXTRA_OPTIONS, options.id)
    val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out).getString(PluginContract.EXTRA_ENTRY)
    assert(new File(result).canExecute)
    result
  }

  private def initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String =
    cr.query(uri, Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) match {
      case null => null
      case cursor =>
        var initialized = false
        val pluginDir = new File(app.getFilesDir, "plugin")
        def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found")
        if (!cursor.moveToFirst()) entryNotFound()
        IOUtils.deleteRecursively(pluginDir)
        if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
        val pluginDirPath = pluginDir.getAbsolutePath + '/'
        val list = new ListBuffer[String]
        do {
          val path = cursor.getString(0)
          val file = new File(pluginDir, path)
          assert(file.getAbsolutePath.startsWith(pluginDirPath))
          autoClose(cr.openInputStream(uri.buildUpon().path(path).build()))(in =>
            autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
          list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath))
          if (path == options.id) initialized = true
        } while (cursor.moveToNext())
        if (!initialized) entryNotFound()
        Shell.SH.run(list)
        new File(pluginDir, options.id).getAbsolutePath
    }
}
