package de.markusfisch.android.binaryeye.activity

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
import de.markusfisch.android.binaryeye.text.unescape
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import java.io.InputStream
import kotlin.math.min

class EncodeActivity : AbstractBaseActivity() {
	private lateinit var formatView: Spinner
	private lateinit var formatDescriptionView: TextView
	private lateinit var ecLabel: TextView
	private lateinit var ecSpinner: Spinner
	private lateinit var colorsLabel: TextView
	private lateinit var colorsSpinner: Spinner
	private lateinit var addQuietZoneSwitch: SwitchCompat
	private lateinit var contentView: EditText
	private lateinit var unescapeCheckBox: CheckBox

	private val writeableFormats = arrayListOf(
		BarcodeFormat.Aztec,
		BarcodeFormat.Codabar,
		BarcodeFormat.Code39Std,
		BarcodeFormat.Code39Ext,
		BarcodeFormat.Code32,
		BarcodeFormat.PZN,
		BarcodeFormat.Code93,
		BarcodeFormat.Code128,
		BarcodeFormat.DataBar,
		BarcodeFormat.DataBarOmni,
		BarcodeFormat.DataBarStk,
		BarcodeFormat.DataBarLtd,
		BarcodeFormat.DataBarExp,
		BarcodeFormat.DataBarExpStk,
		BarcodeFormat.DataMatrix,
		BarcodeFormat.DXFilmEdge,
		BarcodeFormat.EAN8,
		BarcodeFormat.EAN13,
		BarcodeFormat.ITF,
		BarcodeFormat.MaxiCode,
		BarcodeFormat.PDF417,
		BarcodeFormat.QRCode,
		BarcodeFormat.MicroQRCode,
		BarcodeFormat.RMQRCode,
		BarcodeFormat.UPCA,
		BarcodeFormat.UPCE,
	)

	private var bytes: ByteArray? = null

	override fun onActivityResult(
		requestCode: Int,
		resultCode: Int,
		resultData: Intent?
	) {
		super.onActivityResult(requestCode, resultCode, resultData)
		when (requestCode) {
			PICK_FILE_RESULT_CODE -> {
				if (resultCode == Activity.RESULT_OK &&
					resultData != null &&
					resultData.data != null
				) {
					hideSoftKeyboard(contentView)
					val uri = resultData.data ?: return
					bytes = contentResolver?.openInputStream(uri)?.use {
						it.readBytesMax(4096)
					} ?: return
					setEncodeByteArray()
				}
			}
		}
	}

	override fun onCreate(state: Bundle?) {
		super.onCreate(state)
		setScreenContentView(R.layout.activity_encode)
		setTitle(R.string.compose_barcode)

		formatView = findViewById(R.id.format)
		formatDescriptionView = findViewById(R.id.format_description)
		val formatAdapter = ArrayAdapter(
			this,
			android.R.layout.simple_spinner_item,
			writeableFormats.map { it.name.prettifyFormatName() }
		)
		formatAdapter.setDropDownViewResource(
			android.R.layout.simple_spinner_dropdown_item
		)
		formatView.adapter = formatAdapter
		formatView.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
			override fun onItemSelected(
				parentView: AdapterView<*>?,
				selectedItemView: View?,
				position: Int,
				id: Long
			) {
				val format = writeableFormats[position]
				val descResId = format.name.toFormatDescriptionResId()
				if (descResId != 0) {
					formatDescriptionView.setText(descResId)
					formatDescriptionView.visibility = View.VISIBLE
				} else {
					formatDescriptionView.visibility = View.GONE
				}
				val arrayId = when (format) {
					BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
					BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
					BarcodeFormat.MicroQRCode -> R.array.mqr_error_correction_levels
					BarcodeFormat.RMQRCode -> R.array.rmqr_error_correction_levels
					BarcodeFormat.PDF417 -> R.array.pdf417_error_correction_levels
					else -> 0
				}
				if (arrayId > 0) {
					ecSpinner.setEntries(arrayId)
					val idx = format.unpackEcLevel(
						prefs.indexOfLastSelectedEcLevel
					)
					if (idx < ecSpinner.adapter.count) {
						ecSpinner.setSelection(idx)
					}
					true
				} else {
					false
				}.setVisibilityFor(ecLabel, ecSpinner)
				format.canBeInverted().setVisibilityFor(
					colorsLabel, colorsSpinner
				)
			}

			override fun onNothingSelected(parentView: AdapterView<*>?) {}
		}

		ecLabel = findViewById(R.id.error_correction_label)
		ecSpinner = findViewById(R.id.error_correction_level)
		ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
			override fun onItemSelected(
				parentView: AdapterView<*>?,
				selectedItemView: View?,
				position: Int,
				id: Long
			) {
				val format = writeableFormats[formatView.selectedItemPosition]
				prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
					prefs.indexOfLastSelectedEcLevel,
					position
				)
			}

			override fun onNothingSelected(parentView: AdapterView<*>?) {}
		}

		colorsLabel = findViewById(R.id.colors_label)
		colorsSpinner = findViewById(R.id.colors)
		addQuietZoneSwitch = findViewById(R.id.add_quiet_zone)
		addQuietZoneSwitch.isChecked = prefs.addQuietZone

		contentView = findViewById(R.id.content)
		unescapeCheckBox = findViewById(R.id.unescape)
		unescapeCheckBox.isChecked = prefs.expandEscapeSequences

		applyIntent(state == null)

		findViewById<View>(R.id.encode).setOnClickListener {
			it.context.encode()
		}

		findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
		findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
	}

	override fun onNewIntent(intent: Intent) {
		super.onNewIntent(intent)
		setIntent(intent)
		applyIntent(true)
	}

	override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
		inflater.inflate(R.menu.fragment_encode, menu)
	}

	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		return when (item.itemId) {
			R.id.pick_file -> {
				startActivityForResult(
					Intent.createChooser(
						Intent(Intent.ACTION_GET_CONTENT).apply {
							type = "*/*"
						},
						getString(R.string.pick_file)
					),
					PICK_FILE_RESULT_CODE
				)
				true
			}

			else -> super.onOptionsItemSelected(item)
		}
	}

	override fun onPause() {
		super.onPause()
		prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
		prefs.expandEscapeSequences = unescapeCheckBox.isChecked
		prefs.addQuietZone = addQuietZoneSwitch.isChecked
	}

	private fun applyIntent(applyDefaultFormat: Boolean) {
		val args = Bundle().apply {
			intent?.extras?.let { putAll(it) }
			getExtrasFromDeeplink()?.let { putAll(it) }
		}.takeIf { !it.isEmpty }
		val hasTextContent = args?.containsKey(CONTENT_TEXT) == true
		val hasRawContent = args?.containsKey(CONTENT_RAW) == true
		var complete = bytes?.isNotEmpty() == true ||
				contentView.text?.isNotEmpty() == true

		if (hasTextContent || hasRawContent) {
			bytes = null
			contentView.isEnabled = true
			contentView.hint = null
			unescapeCheckBox.isEnabled = true
		}

		if (hasTextContent) {
			args.getString(CONTENT_TEXT)?.let {
				contentView.setText(it)
				complete = it.isNotEmpty()
			}
		}
		if (hasRawContent) {
			args.getByteArray(CONTENT_RAW)?.let {
				bytes = it
				complete = it.isNotEmpty()
				setEncodeByteArray()
			}
		}

		val barcodeFormat = args?.getString(FORMAT)
		if (barcodeFormat != null) {
			formatView.setSelection(
				writeableFormats.indexOf(barcodeFormat.toFormat())
			)
		} else if (applyDefaultFormat) {
			formatView.post {
				formatView.setSelection(prefs.indexOfLastSelectedFormat)
			}
		}

		if (complete &&
			args?.getBoolean(EXECUTE, false) == true
		) {
			contentView.post {
				val content = contentView.context.getContent() ?: return@post
				startActivity(newBarcodeIntent(content))
				finish()
			}
		}
	}

	private fun Context.encode() {
		hideSoftKeyboard(contentView)
		val content = getContent() ?: return
		contentView.post {
			startActivity(newBarcodeIntent(content))
		}
	}

	private fun newBarcodeIntent(content: Any): Intent {
		val format = writeableFormats[formatView.selectedItemPosition]
		return BarcodeActivity.newIntent(
			this,
			content,
			format,
			format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
			addQuietZoneSwitch.isChecked,
			if (format.canBeInverted()) {
				colorsSpinner.selectedItemPosition
			} else 0
		)
	}

	private fun Context.getContent(): Any? {
		if (bytes != null) {
			return bytes
		}
		var text = contentView.text.toString()
		if (text.isEmpty()) {
			toast(R.string.error_no_content)
			return null
		}
		if (!unescapeCheckBox.isChecked) {
			return text
		}
		try {
			text = text.unescape()
		} catch (e: IllegalArgumentException) {
			toast(e.message ?: "Invalid escape sequence")
			return null
		}
		// Return a ByteArray if there were escape sequences for
		// non-printable characters.
		return if (text.any { it.code < 32 && it.code !in setOf(9, 10, 13) }) {
			// Do not expand UTF-8 sequences in binary data.
			text.toByteArray(Charsets.ISO_8859_1)
		} else {
			text
		}
	}

	private fun setEncodeByteArray() {
		contentView.text = null
		contentView.hint = getString(R.string.binary_data)
		contentView.isEnabled = false
		unescapeCheckBox.isEnabled = false
	}

	private fun getExtrasFromDeeplink(): Bundle? {
		val data = intent?.dataString ?: return null
		if (!isEncodeDeeplink(data)) {
			return null
		}
		val uri = data.toUri()
		return Bundle().apply {
			uri.getQueryParameter("content")?.let {
				putString(CONTENT_TEXT, it)
			}
			uri.getQueryParameter("format")?.let {
				putString(FORMAT, it)
			}
			putBoolean(
				EXECUTE,
				uri.getQueryParameter("execute").let {
					it == "" || it?.toBoolean() == true
				}
			)
		}
	}

	companion object {
		private const val CONTENT_TEXT = "content_text"
		private const val CONTENT_RAW = "content_raw"
		private const val FORMAT = "format"
		private const val EXECUTE = "execute"
		private const val PICK_FILE_RESULT_CODE = 1

		fun <T> newIntent(
			context: Context,
			content: T? = null,
			format: String? = null,
			execute: Boolean = false
		): Intent {
			val args = Bundle()
			content?.let {
				when (content) {
					is String -> args.putString(CONTENT_TEXT, content)
					is ByteArray -> args.putByteArray(CONTENT_RAW, content)
					else -> throw IllegalArgumentException(
						"content must be a String or a ByteArray"
					)
				}
				args.putBoolean(EXECUTE, execute)
			}
			format?.let { args.putString(FORMAT, it) }
			return Intent(context, EncodeActivity::class.java).apply {
				putExtras(args)
			}
		}
	}
}

private fun Boolean.setVisibilityFor(vararg views: View) {
	val visibility = if (this) View.VISIBLE else View.GONE
	for (view in views) {
		view.visibility = visibility
	}
}

private fun BarcodeFormat.packEcLevel(packed: Int, level: Int): Int {
	val s = ecLevelShift()
	return (level shl s) or (packed and (15 shl s).inv())
}

private fun BarcodeFormat.unpackEcLevel(packed: Int) =
	(packed shr ecLevelShift()) and 15

private fun BarcodeFormat.ecLevelShift() = when (this) {
	BarcodeFormat.Aztec -> 0
	BarcodeFormat.QRCode -> 4
	BarcodeFormat.MicroQRCode -> 3
	BarcodeFormat.RMQRCode -> 2
	BarcodeFormat.PDF417 -> 8
	else -> throw IllegalArgumentException("$this does not have error levels")
}

private fun BarcodeFormat.canBeInverted() = when (this) {
	BarcodeFormat.Aztec,
	BarcodeFormat.DataMatrix,
	BarcodeFormat.QRCode,
	BarcodeFormat.MicroQRCode,
	BarcodeFormat.RMQRCode -> true

	else -> false
}

private fun String.toFormat(
	default: BarcodeFormat = BarcodeFormat.QRCode
): BarcodeFormat = try {
	BarcodeFormat.valueOf(this)
} catch (_: IllegalArgumentException) {
	default
}

private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
	BarcodeFormat.Aztec -> position
	BarcodeFormat.QRCode,
	BarcodeFormat.MicroQRCode -> (position + 1) * 2

	BarcodeFormat.RMQRCode -> (position + 1) * 4
	BarcodeFormat.PDF417 -> position
	else -> 0
}.coerceIn(0, 8)

private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
	this.context,
	resId,
	android.R.layout.simple_spinner_item
).also { aa ->
	aa.setDropDownViewResource(
		android.R.layout.simple_spinner_dropdown_item
	)
	adapter = aa
}

private fun InputStream.readBytesMax(max: Int): ByteArray {
	var offset = 0
	var remaining = min(available(), max)
	val result = ByteArray(remaining)
	while (remaining > 0) {
		val read = read(result, offset, remaining)
		if (read < 0) {
			break
		}
		remaining -= read
		offset += read
	}
	return result
}
