package yuku.alkitab.yes2;

import android.util.Log;
import yuku.alkitab.io.BibleReader;
import yuku.alkitab.model.Book;
import yuku.alkitab.model.FootnoteEntry;
import yuku.alkitab.model.PericopeBlock;
import yuku.alkitab.model.XrefEntry;
import yuku.alkitab.util.Ari;
import yuku.alkitab.yes2.compress.SnappyInputStream;
import yuku.alkitab.yes2.io.RandomAccessFileRandomInputStream;
import yuku.alkitab.yes2.io.RandomInputStream;
import yuku.alkitab.yes2.io.Yes2VerseTextDecoder;
import yuku.alkitab.yes2.model.SectionIndex;
import yuku.alkitab.yes2.model.Yes2Book;
import yuku.alkitab.yes2.section.BooksInfoSection;
import yuku.alkitab.yes2.section.FootnotesSection;
import yuku.alkitab.yes2.section.PericopesSection;
import yuku.alkitab.yes2.section.TextSection;
import yuku.alkitab.yes2.section.VersionInfoSection;
import yuku.alkitab.yes2.section.XrefsSection;
import yuku.bintex.BintexReader;
import yuku.bintex.ValueMap;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class Yes2Reader implements BibleReader {
	private static final String TAG = Yes2Reader.class.getSimpleName();

	private RandomAccessFileRandomInputStream file_;
	private SectionIndex sectionIndex_;

	// cached in memory
	private VersionInfoSection versionInfo_;
	private PericopesSection pericopesSection_;
	private TextSectionReader textSectionReader_;
	private XrefsSection xrefsSection_;
	private FootnotesSection footnotesSection_;

	/**
	 * This class simplify many operations regarding reading the verse texts from the yes file.
	 * This stores the offset to the beginning of text section content 
	 * and also understands the text section attributes (compression, encryption etc.)
	 */
	static class TextSectionReader {
		private final RandomAccessFileRandomInputStream file_;
		private final Yes2VerseTextDecoder decoder_;
		private final long sectionContentOffset_;
		private BintexReader br_;
		
		private SnappyInputStream snappyInputStream;  // null means no compression
		
		public TextSectionReader(RandomAccessFileRandomInputStream file, Yes2VerseTextDecoder decoder, ValueMap sectionAttributes, long sectionContentOffset) throws Exception {
			file_ = file;
			decoder_ = decoder;
			sectionContentOffset_ = sectionContentOffset;
			br_ = new BintexReader(null);
			
			if (sectionAttributes != null) {
				String compressionName = sectionAttributes.getString("compression.name");
				if (compressionName != null) {
					if ("snappy-blocks".equals(compressionName)) {
						snappyInputStream = SnappyInputStream.getInstanceFromAttributes(file_, sectionAttributes, sectionContentOffset);
					} else {
						throw new Exception("Compression " + compressionName + " is not supported");
					}
				}
			}
		}

		public Yes2SingleChapterVerses loadVerseText(Yes2Book yes2Book, int chapter_1, boolean dontSeparateVerses, boolean lowercase) throws Exception {
			int contentOffset = yes2Book.offset; 
			contentOffset += yes2Book.chapter_offsets[chapter_1 - 1];
			
			BintexReader br;
			if (snappyInputStream != null) {
				snappyInputStream.seek(contentOffset);
				br = br_.reuse(snappyInputStream);
			} else {
				file_.seek(sectionContentOffset_ + contentOffset);
				br = br_.reuse(file_);
			}

			int verse_count = yes2Book.verse_counts[chapter_1 - 1];
			if (dontSeparateVerses) {
				return new Yes2SingleChapterVerses(new String[] { 
					decoder_.makeIntoSingleString(br, verse_count, lowercase),
				});
			} else {
				return new Yes2SingleChapterVerses(decoder_.separateIntoVerses(br, verse_count, lowercase));
			}
		}
	}
	
	public Yes2Reader(RandomAccessFileRandomInputStream input) {
		this.file_ = input;
	}

	/** Read section index */
	private synchronized void loadSectionIndex() throws IOException {
		if (sectionIndex_ != null) { // we have read it previously.
			return;
		}
			
		file_.seek(0);

		{ // check header
			byte[] buf = new byte[8];
			file_.read(buf);
			if (!Arrays.equals(buf, new byte[] { (byte) 0x98, 0x58, 0x0d, 0x0a, 0x00, 0x5d, (byte) 0xe0, 0x02 /* yes version 2 */})) {
				throw new RuntimeException("YES2: Header is incorrect. Found: " + Arrays.toString(buf));
			}
		}
		
		file_.seek(12); // start of sectionIndex
		sectionIndex_ = SectionIndex.read(file_);
	}
	
	private synchronized void loadVersionInfo() throws Exception {
		if (seekToSection("versionInfo")) {
			versionInfo_ = new VersionInfoSection.Reader().read(file_);
		}
	}
	
	private synchronized boolean seekToSection(String sectionName) throws IOException {
		loadSectionIndex();
		
		if (sectionIndex_ == null) {
			Log.e(TAG, "@@seekToSection Could not load section index");
			return false;
		}
		
		if (sectionIndex_.seekToSectionContent(sectionName, file_)) {
			return true;
		} else {
			Log.e(TAG, "@@seekToSection Could not seek to section: " + sectionName);
			return false;
		}
	}

	@Override
	public String getLocale() {
		try {
			loadVersionInfo();
			return versionInfo_.locale;
		} catch (Exception e) {
			Log.e(TAG, "yes load version info error", e);
			return "";
		}
	}

	@Override public String getShortName() {
		try {
			loadVersionInfo();
			return versionInfo_.shortName;
		} catch (Exception e) {
			Log.e(TAG, "yes load version info error", e);
			return "";
		}
	}

	@Override public String getLongName() {
		try {
			loadVersionInfo();
			return versionInfo_.longName;
		} catch (Exception e) {
			Log.e(TAG, "yes load version info error", e);
			return "";
		}
	}

	@Override
	public String getDescription() {
		try {
			loadVersionInfo();
			return versionInfo_.description;
		} catch (Exception e) {
			Log.e(TAG, "yes load version info error", e);
			return "";
		}
	}

	@Override public Book[] loadBooks() {
		try {
			loadVersionInfo();

			if (seekToSection(BooksInfoSection.SECTION_NAME)) {
				BooksInfoSection section = new BooksInfoSection.Reader().read(file_);
				List<Yes2Book> books = section.yes2Books;
				return books.toArray(new Yes2Book[books.size()]);
			}
			
			Log.e(TAG, "no section named " + BooksInfoSection.SECTION_NAME);
			return null;
		} catch (Exception e) {
			Log.e(TAG, "loadBooks error", e);
			return null;
		}
	}

	@Override public Yes2SingleChapterVerses loadVerseText(Book book, int chapter_1, boolean dontSeparateVerses, boolean lowercase) {
		Yes2Book yes2Book = (Yes2Book) book;
		
		try {
			if (chapter_1 <= 0 || chapter_1 > yes2Book.chapter_count) {
				return null;
			}

			if (textSectionReader_ == null) {
				// init text decoder 
				Yes2VerseTextDecoder decoder;
				int textEncoding = versionInfo_.textEncoding;
				if (textEncoding == 1) {
					decoder = new Yes2VerseTextDecoder.Ascii();
				} else if (textEncoding == 2) {
					decoder = new Yes2VerseTextDecoder.Utf8();
				} else {
					Log.e(TAG, "Text encoding " + textEncoding + " not supported! Fallback to ascii.");
					decoder = new Yes2VerseTextDecoder.Ascii();
				}
				
				ValueMap sectionAttributes = sectionIndex_.getSectionAttributes(TextSection.SECTION_NAME, file_);
				long sectionContentOffset = sectionIndex_.getAbsoluteOffsetForSectionContent(TextSection.SECTION_NAME);
				textSectionReader_ = new TextSectionReader(file_, decoder, sectionAttributes, sectionContentOffset);
			}
			
			return textSectionReader_.loadVerseText(yes2Book, chapter_1, dontSeparateVerses, lowercase);
		} catch (Exception e) {
			Log.e(TAG, "@@loadVerseText error book=" + book + " chapter_1=" + chapter_1 + " dontSeparateVerses=" + dontSeparateVerses + " lowercase=" + lowercase, e);
			return null;
		}
	}

	@Override public int loadPericope(final int bookId, final int chapter_1, final List<Integer> aris, final List<PericopeBlock> pericopeBlocks) {
		try {
			loadVersionInfo();
			
			if (versionInfo_.hasPericopes == 0) {
				return 0;
			}
			
			if (pericopesSection_ == null) { // not yet loaded!
				final RandomInputStream sectionInput = prepareLoadSection(PericopesSection.SECTION_NAME);

				if (sectionInput == null) {
					return 0;
				}

				pericopesSection_ = new PericopesSection.Reader().read(sectionInput);
			}
			
			if (pericopesSection_ == null) { 
				Log.e(TAG, "Didn't succeed in loading pericopes section");
				return 0;
			}
		
			int ariMin = Ari.encode(bookId, chapter_1, 0);
			int ariMax = Ari.encode(bookId, chapter_1 + 1, 0);

			return pericopesSection_.getPericopesForAris(ariMin, ariMax, aris, pericopeBlocks);
		} catch (Exception e) {
			Log.e(TAG, "General exception in loading pericope block", e);
			return 0;
		}
	}

	@Override
	public XrefEntry getXrefEntry(int arif) {
		if (xrefsSection_ == null) { // not yet loaded!
			try {
				final RandomInputStream sectionInput = prepareLoadSection(XrefsSection.SECTION_NAME);
				if (sectionInput == null) {
					return null;
				}

				xrefsSection_ = new XrefsSection.Reader().read(sectionInput);
			} catch (Exception e) {
				Log.e(TAG, "General exception in loading xref section", e);
				return null;
			}
		}

		return xrefsSection_.getXrefEntry(arif);
	}

	@Override
	public FootnoteEntry getFootnoteEntry(final int arif) {
		if (footnotesSection_ == null) { // not yet loaded!
			try {
				final RandomInputStream sectionInput = prepareLoadSection(FootnotesSection.SECTION_NAME);
				if (sectionInput == null) {
					return null;
				}

				footnotesSection_ = new FootnotesSection.Reader().read(sectionInput);
			} catch (Exception e) {
				Log.e(TAG, "General exception in loading footnote section", e);
				return null;
			}
		}

		return footnotesSection_.getFootnoteEntry(arif);
	}

	/**
	 * Prepares an input stream for a section.
	 * @return an input stream that transparently handles compressed/uncompressed data. Null if the section name is not found.
	 */
	RandomInputStream prepareLoadSection(final String sectionName) throws IOException {
		final ValueMap sectionAttributes = sectionIndex_.getSectionAttributes(sectionName, file_);
		final long sectionContentOffset = sectionIndex_.getAbsoluteOffsetForSectionContent(sectionName);

		RandomInputStream sectionInput = null;
		if (sectionAttributes != null) {
			String compressionName = sectionAttributes.getString("compression.name");
			if (compressionName != null) {
				if ("snappy-blocks".equals(compressionName)) {
					sectionInput = SnappyInputStream.getInstanceFromAttributes(file_, sectionAttributes, sectionContentOffset);
				} else {
					throw new IOException("Compression " + compressionName + " is not supported");
				}
			}
		}

		// no compression detected
		if (sectionInput == null) {
			sectionInput = file_;
		}

		if (seekToSection(sectionName)) {
			return sectionInput;
		} else {
			return null;
		}
	}
}
