package net.pokeranalytics.android.util.csv

import io.realm.Realm
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.io.FileReader
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader

class ImportException(message: String) : Exception(message)

/**
 * A CSVImporter is a class in charge of parsing a CSV file and processing it
 * When starting the parsing of a file, the instance will search for a CSVDescriptor, which describes
 * the format of a CSV file.
 * When finding a descriptor, the CSVImporter then continue to parse the file and delegates the parsing of each row
 * to the CSVDescriptor
 */
open class CSVImporter(istream: InputStream) {

	/**
	 * Number of commits required to commit a Realm transaction
	 */
	private val COMMIT_FREQUENCY = 100
	/**
	 * The number of column indicating a valid record
	 */
	private val VALID_RECORD_COLUMNS = 4
	/**
	 * The number of valid record to test for descriptor before throwing a File Format Exception
	 */
	private val VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION = 4

	/**
	 * The path of the CSV file
	 */
	private var path: String? = null
	/**
	 * The InputStream containing a file content
	 */
	private var inputStream: InputStream? = istream

	/**
	 * The current number of attempts at finding a valid CSVDescriptor
	 */
	private var descriptorFindingAttempts = 0

	/**
	 * Stores the descriptors found
	 */
	private var usedDescriptors: MutableList<CSVDescriptor> = mutableListOf()
	/**
	 * The currently used CSVDescriptor for parsing
	 */
	private var currentDescriptor: CSVDescriptor? = null

	/**
	 * Constructs a CSVParser object and starts parsing the CSV
	 */
	fun start() {

		val realm = Realm.getDefaultInstance()

		var reader: Reader? = null
		if (this.path != null) {
			reader = FileReader(this.path)
		}
		if (this.inputStream != null) {
			reader = InputStreamReader(this.inputStream)
		}

		val parser = CSVFormat.DEFAULT.withAllowMissingColumnNames().parse(reader)

		Timber.d("Starting import...")

		realm.beginTransaction()

		parser.forEachIndexed { index, record ->

			Timber.d("line $index")

			if (this.currentDescriptor == null) { // find descriptor
				this.currentDescriptor = this.findDescriptor(record)

				if (this.currentDescriptor == null) {

					if (record.size() >= VALID_RECORD_COLUMNS) {
						this.descriptorFindingAttempts++
					}
					if (this.descriptorFindingAttempts >= VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION) {
						realm.cancelTransaction()
						throw ImportException("This type of file is not supported")
					}
				}

			} else { // parse

				val parsingIndex = index + 1
				if (parsingIndex % COMMIT_FREQUENCY == 0) {
					Timber.d("****** committing at $parsingIndex sessions...")
					realm.commitTransaction()
					realm.beginTransaction()
				}

				this.currentDescriptor?.let {
					if (record.size() == 0) {
						this.usedDescriptors.add(it)
						this.currentDescriptor = null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file)
						this.descriptorFindingAttempts = 0
					} else {
						it.parse(realm, record)
					}
				} ?: run {
					realm.cancelTransaction()
					throw IllegalStateException("CSVDescriptor should never be null here")
				}

			}
		}

		realm.commitTransaction()

		Timber.d("Ending import...")

		realm.close()
	}

	/**
	 * Search for a descriptor in the list of managed formats
	 */
	private fun findDescriptor(record: CSVRecord) : CSVDescriptor? {

		CSVDescriptor.all.forEach { descriptor ->
			if (descriptor.matches(record)) {
				this.currentDescriptor = descriptor
				Timber.d("Identified source: ${descriptor.source}")
				return descriptor
			}
		}
		return null
	}

	fun save(realm: Realm) {

		this.usedDescriptors.forEach { descriptor ->

			if (descriptor is DataCSVDescriptor<*>) {
				realm.executeTransaction {
					realm.copyToRealm(descriptor.realmModels)
				}
			}
		}

	}

}
