// Copyright (C) 2011-2012 CRS4.
//
// This file is part of Seal.
//
// Seal is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// Seal is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with Seal.  If not, see <http://www.gnu.org/licenses/>.

package it.crs4.seal.common;

import java.util.List;
import java.util.Set;
import java.nio.ByteBuffer;
import java.io.UnsupportedEncodingException;

/**
 * Simple in-memory mapping
 */
public class WritableMapping extends AbstractTaggedMapping
{
	////////////////////////////////////////////////
	// variables
	////////////////////////////////////////////////
	protected String        name;
	protected int           flag       = 0x4; // unmapped bit set.  All the rest at 0
	protected String        contig;
	protected int           pos        = 0;
	protected int           mapq       = 255;
	protected List<AlignOp> alignment;
	protected ByteBuffer    sequence;
	protected ByteBuffer    quality;
	protected int           insertSize = 0;

	public WritableMapping() { }

	/**
	 * Construct a simple mapping from ASCII-encoded sequence and base quality scores.
	 */
	public WritableMapping(String name, String seq, String qual)
	{
		if (seq.length() != qual.length())
			throw new IllegalArgumentException(String.format("sequence and quality strings have different lengths! (%d and %d)", seq.length(), qual.length()));

		this.name = name;
		setSequence(seq);
		setBaseQualities(qual);
	}

	public WritableMapping(String name, ByteBuffer seq, ByteBuffer qual)
	{
		this.name = name;
		this.sequence = seq;
		this.quality = qual;
	}

	////////////////////////////////////////////////
	// methods
	////////////////////////////////////////////////
	public String getName() { return name; }
	public void setName(String name) { this.name = name; }

	public int getFlag() { return flag; }

	public void setFlag(int flag)
	{
		this.flag = flag;

		if (isUnmapped())
			mapq = 0;

		if (isUnmapped() || !isPaired() || isMateUnmapped())
			insertSize = 0;
	}

	public String getContig() throws IllegalStateException
	{
		if (isUnmapped())
			throw new IllegalStateException();
		return contig;
	}

	public void setContig(String contig) { this.contig = contig; }

	public int get5Position() throws IllegalStateException
	{
		if (isUnmapped())
			throw new IllegalStateException();
	 	return pos;
	}

	public void set5Position(int p) { pos = p; }

	public int getMapQ()
	{
		if (isUnmapped())
			throw new IllegalStateException();
	 	return mapq;
	}

	public void setMapQ(int q)
	{
		if (q < 0 || q > 255)
			throw new IllegalArgumentException("mapq must be between 0 and 255 (got " + q + ")");
		mapq = q;
	}

	public List<AlignOp> getAlignment() throws IllegalStateException
	{
		if (isUnmapped())
			throw new IllegalStateException();

		return alignment;
	}

	public void setAlignment(List<AlignOp> align) { alignment = align; }

	public ByteBuffer getSequence() { return sequence; }

	/**
	 * Set buf as this mapping's sequence.
	 *
	 * A reference to buf is kept by this object.
	 */
	public void setSequence(ByteBuffer buf) { sequence = buf; }

	/**
	 * Set this mapping's sequence to the contents of seq.
	 *
	 * The byte array seq is copied to this object's internal ByteBuffer.
	 */
	public void setSequence(byte[] seq, int offset, int length)
	{
		sequence = ByteBuffer.allocate(length);
		sequence.put(seq, offset, length).rewind().mark();
	}

	public void setSequence(byte[] seq)
	{
		setSequence(seq, 0, seq.length);
	}

	/**
	 * Set this mapping's sequence to the contents of seq.
	 *
	 * A new ByteBuffer is created from the contents of the seq string and
	 * it is set as this mapping's sequence.
	 */
	public void setSequence(String seq)
	{
		try {
			sequence = ByteBuffer.wrap(seq.getBytes("US-ASCII"));
			sequence.mark();
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("Can't get US-ASCII charset!");
		}
	}

	public ByteBuffer getBaseQualities() { return quality; }

	/**
	 * Set buf as this mapping's base qualities.
	 *
	 * A reference to buf is kept by this object.
	 */
	public void setBaseQualities(ByteBuffer buf) { quality = buf; }

	/**
	 * Set this mapping's base qualities to the contents of seq.
	 *
	 * The byte array seq is copied to this object's internal ByteBuffer.
	 */
	public void setBaseQualities(byte[] seq, int offset, int length)
	{
		quality = ByteBuffer.allocate(length);
		quality.put(seq, offset, length).rewind().mark();
	}

	/**
	 * Set this mapping's base qualities to the contents of seq.
	 *
	 * The byte array seq is copied to this object's internal ByteBuffer.
	 */
	public void setBaseQualities(byte[] seq)
	{
		setBaseQualities(seq, 0, seq.length);
	}

	/**
	 * Set this mapping's base qualities to the contents of seq.
	 *
	 * A new ByteBuffer is created from the contents of the seq string and
	 * it is set as this mapping's base qualities.
	 */
	public void setBaseQualities(String seq)
	{
		try {
			quality = ByteBuffer.wrap(seq.getBytes("US-ASCII"));
			quality.mark();
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("Can't get US-ASCII charset!");
		}
	}

	public int getLength()
	{
		return sequence.limit() - sequence.position();
	}

	/**
	 * Clear this mapping.
	 *
	 * Returns it to the state of a new mapping allocated with the default constructor.
	 */
	public void clear()
	{
		name       = null;
		flag       = 0x4;
		contig     = null;
		pos        = 0;
		mapq       = 255;
		alignment  = null;
		sequence   = null;
		quality    = null;
		insertSize = 0;
		tagCache.clear();
	}

	protected TagCacheItem makeTagItem(String name) throws NoSuchFieldException
	{
		throw new NoSuchFieldException("no field named " + name);
	}

	public void setTag(String name, TagDataType type, String value)
	{
		tagCache.put(name, new TagCacheItem(type, value));
	}

	public Set<String> getTagNames()
	{
		return tagCache.keySet();
	}

	//////////////////////// template/insert size methods ////////////////////////

	public void setTemplateLength(int isize)
	{
		if (isize != 0)
		{
			if (isUnmapped() || !isPaired() || isMateUnmapped())
				throw new IllegalStateException("Setting non-zero template size for read in incompatible state");
		}
		insertSize = isize;
	}

	public boolean isTemplateLengthAvailable() { return insertSize != 0; }

	public int getTemplateLength() throws IllegalStateException
	{
		if (!isTemplateLengthAvailable())
			throw new IllegalStateException();
		return insertSize;
	}

	//////////////////////// flag set methods ////////////////////////

	public void setIsPaired(boolean v) {
		setFlag(v, AlignFlags.Paired);
	}

	public void setIsProperlyPaired(boolean v) {
		setFlag(v, AlignFlags.ProperlyPaired);
	}

	public void setIsMapped(boolean v) {
		setFlag(!v, AlignFlags.Unmapped);
	}

	public void setIsUnmapped(boolean v) {
		setFlag(v, AlignFlags.Unmapped);
	}

	public void setIsMateMapped(boolean v) {
		setFlag(!v, AlignFlags.MateUnmapped);
	}

	public void setIsMateUnmapped(boolean v) {
		setFlag(v, AlignFlags.MateUnmapped);
	}

	public void setIsOnReverse(boolean v) {
		setFlag(v, AlignFlags.OnReverse);
	}

	public void setIsMateOnReverse(boolean v) {
		setFlag(v, AlignFlags.MateOnReverse);
	}

	public void setIsRead1(boolean v) {
		setFlag(v, AlignFlags.Read1);
	}

	public void setIsRead2(boolean v) {
		setFlag(v, AlignFlags.Read2);
	}

	public void setIsSecondaryAlign(boolean v) {
		setFlag(v, AlignFlags.SecondaryAlignment);
	}

	public void setIsFailedQC(boolean v) {
		setFlag(v, AlignFlags.FailedQC);
	}

	public void setIsDuplicate(boolean v) {
		setFlag(v, AlignFlags.Duplicate);
	}

	protected void setFlag(boolean newValue, AlignFlags f)
	{
		if (newValue)
			flag = f.set(flag);
		else
			flag = f.clear(flag);
	}
}
