package de.markusfisch.android.binaryeye.fragment

import android.annotation.SuppressLint
import com.google.zxing.BarcodeFormat

import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.actions.validateOrGetNew
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.shareText

import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.Fragment
import android.text.ClipboardManager
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast

import java.io.File
import java.io.IOException
import java.net.URLEncoder

class DecodeFragment : Fragment() {
	private lateinit var contentView: EditText
	private lateinit var formatView: TextView
	private lateinit var hexView: TextView
	private lateinit var format: BarcodeFormat
	private lateinit var actionMenuItem: MenuItem

	private var action: IAction? = null
	private var isBinary = false
	private val content: String
		get() = contentView.text.toString()

	override fun onCreate(state: Bundle?) {
		super.onCreate(state)
		setHasOptionsMenu(true)
	}

	override fun onCreateView(
		inflater: LayoutInflater,
		container: ViewGroup?,
		state: Bundle?
	): View {
		activity.setTitle(R.string.content)

		val view = inflater.inflate(
			R.layout.fragment_decode,
			container,
			false
		)

		val inputContent = arguments?.getString(CONTENT) ?: ""
		isBinary = hasNonPrintableCharacters(inputContent) or inputContent.isEmpty()
		val raw = arguments?.getByteArray(RAW) ?: inputContent.toByteArray()
		format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE

		contentView = view.findViewById(R.id.content)
		val shareFab = view.findViewById<ImageView>(R.id.share)

		if (!isBinary) {
			contentView.setText(inputContent)
			contentView.addTextChangedListener(object : TextWatcher {
				override fun afterTextChanged(s: Editable?) {
					updateViewsAndAction(content.toByteArray())
				}

				override fun beforeTextChanged(
					s: CharSequence?,
					start: Int,
					count: Int,
					after: Int
				) {
				}

				override fun onTextChanged(
					s: CharSequence?,
					start: Int,
					before: Int,
					count: Int
				) {
				}
			})
			shareFab.setOnClickListener { v ->
				shareText(v.context, content)
			}
		} else {
			contentView.setText(R.string.binary_data)
			contentView.isEnabled = false
			shareFab.setImageResource(R.drawable.ic_action_save)
			shareFab.setOnClickListener {
				askForFileNameAndSave(raw)
			}
		}

		formatView = view.findViewById(R.id.format)
		hexView = view.findViewById(R.id.hex)

		updateViewsAndAction(raw)

		return view
	}

	private fun updateViewsAndAction(bytes: ByteArray) {
		action = action.validateOrGetNew(bytes)
		formatView.text = resources.getQuantityString(
			R.plurals.barcode_info,
			bytes.size,
			format.toString(),
			bytes.size
		)
		hexView.text = hexDump(bytes, 33)
		if (::actionMenuItem.isInitialized) {
			actionMenuItem.setIcon(action?.iconResId ?: R.drawable.ic_action_open)
			actionMenuItem.setTitle(action?.titleResId ?: R.string.open_url)
		}
	}

	override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
		inflater.inflate(R.menu.fragment_decode, menu)
		actionMenuItem = menu.findItem(R.id.open_url)
		action?.also { action ->
			actionMenuItem.setIcon(action.iconResId)
			actionMenuItem.setTitle(action.titleResId)
		}
		if (isBinary) {
			menu.findItem(R.id.copy_to_clipboard).isVisible = false
			menu.findItem(R.id.open_url).isVisible = false
			menu.findItem(R.id.create).isVisible = false
		}
	}

	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		return when (item.itemId) {
			R.id.copy_to_clipboard -> {
				copyToClipboard(content)
				true
			}
			R.id.open_url -> {
				openUrl(content)
				true
			}
			R.id.create -> {
				addFragment(
					fragmentManager,
					EncodeFragment.newInstance(content, format)
				)
				true
			}
			else -> super.onOptionsItemSelected(item)
		}
	}

	private fun copyToClipboard(text: String) {
		activity ?: return

		val cm = activity.getSystemService(
			Context.CLIPBOARD_SERVICE
		) as ClipboardManager
		cm.text = text
		Toast.makeText(
			activity,
			R.string.put_into_clipboard,
			Toast.LENGTH_SHORT
		).show()
	}

	private fun openUrl(
		url: String,
		executeCustomAction: Boolean = true,
		searchIfNoUrl: Boolean = true
	) {
		if (activity == null || url.isEmpty()) {
			return
		}
		var uri = Uri.parse(url)
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
			uri = uri.normalizeScheme()
		}
		val intent = Intent(Intent.ACTION_VIEW, uri)
		when {
			executeCustomAction && action != null -> action?.also { action ->
				action.execute(activity, url.toByteArray())
			}
			intent.resolveActivity(activity.packageManager) != null -> {
				startActivity(intent)
			}
			searchIfNoUrl -> pickSearchEngineAndSearch(activity, url)
			else -> Toast.makeText(
				activity,
				R.string.cannot_resolve_action,
				Toast.LENGTH_SHORT
			).show()
		}
	}

	private fun pickSearchEngineAndSearch(context: Context, query: String) {
		val names = context.resources.getStringArray(
			R.array.search_engines_names
		).toMutableList()
		val urls = context.resources.getStringArray(
			R.array.search_engines_values
		).toMutableList()
		if (prefs.openWithUrl.isNotEmpty()) {
			names.add(prefs.openWithUrl)
			urls.add(prefs.openWithUrl)
		}
		AlertDialog.Builder(context)
			.setTitle(R.string.pick_search_engine)
			.setItems(names.toTypedArray()) { _, which ->
				openUrl(
					urls[which] + URLEncoder.encode(query, "utf-8"),
					executeCustomAction = false,
					searchIfNoUrl = false
				)
			}
			.show()
	}

	// dialogs don't have a parent layout and must therefore be
	// inflated with a null root layout
	@SuppressLint("InflateParams")
	private fun askForFileNameAndSave(raw: ByteArray) {
		val ac = activity ?: return
		val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
		val editText = view.findViewById<EditText>(R.id.file_name)
		AlertDialog.Builder(ac)
			.setView(view)
			.setPositiveButton(android.R.string.ok) { _, _ ->
				if (hasWritePermission(ac)) {
					val messageId = saveByteArray(
						editText.text.toString(),
						raw
					)
					if (messageId > 0) {
						Toast.makeText(
							ac, messageId,
							Toast.LENGTH_SHORT
						).show()
					}
				}
			}
			.show()
	}

	companion object {
		private const val CONTENT = "content"
		private const val FORMAT = "format"
		private const val RAW = "raw"

		fun newInstance(
			content: String,
			format: BarcodeFormat,
			raw: ByteArray? = null
		): Fragment {
			val args = Bundle()
			args.putString(CONTENT, content)
			args.putSerializable(FORMAT, format)
			if (raw != null) {
				args.putByteArray(RAW, raw)
			}
			val fragment = DecodeFragment()
			fragment.arguments = args
			return fragment
		}
	}
}

private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
	if (charsPerLine < 4 || bytes.isEmpty()) {
		return ""
	}
	val dump = StringBuilder()
	val hex = StringBuilder()
	val ascii = StringBuilder()
	val itemsPerLine = (charsPerLine - 1) / 4
	val len = bytes.size
	var i = 0
	while (true) {
		val ord = bytes[i]
		hex.append(String.format("%02X ", ord))
		ascii.append(if (ord > 31) ord.toChar() else " ")

		++i

		val posInLine = i % itemsPerLine
		val atEnd = i >= len
		var atLineEnd = posInLine == 0
		if (atEnd && !atLineEnd) {
			for (j in posInLine until itemsPerLine) {
				hex.append("   ")
			}
			atLineEnd = true
		}
		if (atLineEnd) {
			dump.append(hex.toString())
			dump.append(" ")
			dump.append(ascii.toString())
			dump.append("\n")
			hex.setLength(0)
			ascii.setLength(0)
		}
		if (atEnd) {
			break
		}
	}
	return dump.toString()
}

fun saveByteArray(name: String, raw: ByteArray): Int {
	return try {
		val file = File(
			Environment.getExternalStoragePublicDirectory(
				Environment.DIRECTORY_DOWNLOADS
			),
			name
		)
		if (file.exists()) {
			return R.string.error_file_exists
		}
		file.writeBytes(raw)
		0
	} catch (e: IOException) {
		R.string.error_saving_binary_data
	}
}
