<?php
/**
 * bgMACSIMSFormat
 * @package biogagniere
 * @subpackage io
 * @author Nicolas Gagniere <gagniere@igbmc.fr>
 * @version 0.1
 */
class bgMACSIMSFormat {
    const DTD_TYPE = 'macsim';
    const DTD_SYSTEM_ID = 'http://www-bio3d-igbmc.u-strasbg.fr/macsim.dtd';

    const ANNOTATION_LIFE_DOMAIN = 'bgMACSIMSFormat.LIFE_DOMAIN';
    const ANNOTATION_HYDROPHOBICITY = 'bgMACSIMSFormat.HYDROPHOBICITY';
    const ANNOTATION_FEATURE_NOTE = 'bgMACSIMSFormat.FEATURE_NOTE';
    const ANNOTATION_FEATURE_COLOR = 'bgMACSIMSFormat.FEATURE_COLOR';
    const ANNOTATION_FEATURE_SCORE = 'bgMACSIMSFormat.FEATURE_SCORE';

    /**
     * @var bgColorPalette
     */
    private static $colorPalette;
    /**
     * @var int
     */
    private static $availableColors;

    public function  __construct() {
    }

    /**
     *
     * @param string $filename file to parse
     * @return bgAlignment alignment of bgSequence
     * @throws bgFileNotFoundException
     */
    public static function read($filename) {
        $string = @file_get_contents($filename);
        if($string === false) {
            throw new bgFileNotFoundException($filename);
        }
        return self::parse($string);
    }

    /**
     *
     * @param resource $fh file or stream resource to parse
     * @return bgAlignment alignment of bgSequence
     * @throws bgIllegalArgumentException
     * @throws bgIOException
     */
    public static function readResource($fh) {
        $type = get_resource_type($fh);
        if(!($type == 'file' || $type == 'stream')) {
            throw new bgIllegalArgumentException('$fh');
        }
        $string = stream_get_contents($fh);
        if($string === false) {
            throw new bgIOException();
        }
        return self::parse($string);
    }

    /**
     *
     * @param string $string in memory file to parse
     * @return bgAlignment alignment of bgSequence
     */
    public static function readString($string) {
        return self::parse($string);
    }

    /**
     *
     * @param string $string
     * @return array array of bgSequence
     */
    private static function parse($string) {
        $doc = new DOMDocument();
        if(@$doc->loadXML($string) === false) {
            throw new bgParseException('XML not well formed');
        }
        if($doc->documentElement->nodeName != 'macsim') {
            throw new bgParseException('This is not a macsims document');
        }

        $alignment = new bgAlignment();

        foreach($doc->getElementsByTagName('sequence') as $seqNode) {
            // BASIC SEQUENCE CREATION
            $type = self::nodeAttribute($seqNode, 'seq-type');
            $name = self::nodeContent($seqNode->getElementsByTagName('seq-name')->item(0));
            $seqString = self::nodeContent($seqNode->getElementsByTagName('seq-data')->item(0));

            switch($type) {
                case 'Protein':
                    $sequence = new bgProteinSequence($name, $seqString);
                    break;
                case 'DNA':
                    $sequence = new bgNucleicSequence($name, $seqString);
                    break;
                default:
                    $sequence = new bgSequence($name, $seqString);
                    break;
            }

            // SEQUENCE ANNOTATION
            self::setAnnotation($sequence->getAnnotation(), bgSequence::ANNOTATION_ACCESSION, $seqNode->getElementsByTagName('accession')->item(0));
            self::setAnnotation($sequence->getAnnotation(), bgSequence::ANNOTATION_DESCRIPTION, $seqNode->getElementsByTagName('definition')->item(0));
            self::setAnnotation($sequence->getAnnotation(), bgSequence::ANNOTATION_ORGANISM, $seqNode->getElementsByTagName('organism')->item(0));
            self::setAnnotation($sequence->getAnnotation(), bgSequence::ANNOTATION_EC_NUMBER, $seqNode->getElementsByTagName('ec')->item(0));
            self::setAnnotation($sequence->getAnnotation(), bgSequence::ANNOTATION_NCBI_TAXID, $seqNode->getElementsByTagName('taxid')->item(0));
            self::setAnnotation($sequence->getAnnotation(), self::ANNOTATION_LIFE_DOMAIN, $seqNode->getElementsByTagName('lifedomain')->item(0));
            self::setAnnotation($sequence->getAnnotation(), self::ANNOTATION_HYDROPHOBICITY, $seqNode->getElementsByTagName('hydrophobicity')->item(0));
            $oc = array();
            foreach($seqNode->getElementsByTagName('taxon') as $txNode) {
                $oc[] = self::nodeContent($txNode);
            }
            if(!empty($oc)) {
                $sequence->getAnnotation()->setProperty(bgSequence::ANNOTATION_ORGANISM_CLASSIFICATION, $oc);
            }

            // SEQUENCE FEATURES
            foreach($seqNode->getElementsByTagName('fitem') as $ftNode) {
                $ftType = self::nodeContent($ftNode->getElementsByTagName('ftype')->item(0));
                $ftStart = self::nodeContent($ftNode->getElementsByTagName('fstart')->item(0));
                $ftStop = self::nodeContent($ftNode->getElementsByTagName('fstop')->item(0));
                try {
                    $ftLocation = new bgLocation($ftStart, $ftStop);
                } catch(bgIllegalArgumentException $ex) {
                    continue;
                }
                $ftNote = self::nodeContent($ftNode->getElementsByTagName('fnote')->item(0));
                $ftColor = self::nodeContent($ftNode->getElementsByTagName('fcolor')->item(0));
                $ftScore = self::nodeContent($ftNode->getElementsByTagName('fscore')->item(0));

                $ft = new bgFeature($ftType, $ftLocation);
                $ft->setLocationType(bgFeature::LOCATION_TYPE_ABSOLUTE);
                $ft->setLocationNumbering(bgFeature::LOCATION_NUMBERING_RAW_SEQUENCE);
                $sequence->addFeature($ft);
                $ft->getAnnotation()->setProperty(self::ANNOTATION_FEATURE_NOTE, $ftNote);
                $ft->getAnnotation()->setProperty(self::ANNOTATION_FEATURE_COLOR, $ftColor);
                $ft->getAnnotation()->setProperty(self::ANNOTATION_FEATURE_SCORE, $ftScore);
                $ft->getAnnotation()->setProperty(bgFeature::ANNOTATION_BACKGROUND_COLOR, self::getColor($ftColor));
                $ft->getAnnotation()->setProperty(bgFeature::ANNOTATION_DESCRIPTION, $ftNote);
            }

            // ADD SEQUENCE TO THE ALIGNMENT
            $alignment->addSequence($sequence);

            // GROUP SEQUENCE
            $group = self::nodeContent($seqNode->getElementsByTagName('group')->item(0));
            if($group !== null) {
                $alignment->setClusterTo($sequence, $group);
            }
        }

        return $alignment;
    }

    private static function nodeAttribute(DOMNode $node = null, $attribute = '') {
        $attribute = (string) $attribute;
        if($node !== null && $attribute !== '') {
            $content = $node->getAttribute($attribute);
            if($content != '') {
                return $content;
            }
        }

        return null;
    }

    private static function nodeContent(DOMNode $node = null) {
        if($node !== null) {
            $content = trim($node->textContent);
            if($content != '') {
                return $content;
            }
        }

        return null;
    }

    private static function setAnnotation(bgAnnotation $annotation, $property, DOMNode $node = null) {
        $content = self::nodeContent($node);
        if($content !== null) {
            if($property === bgSequence::ANNOTATION_EC_NUMBER) {
                try {
                    $content = new bgECNumber($content);
                } catch(Exception $ex) {
                    return;
                }
            }
            $annotation->setProperty($property, $content);
        }
    }

    private static function getAnnotation(bgSequence $sequence, $property) {
        $value = $sequence->getAnnotation()->getProperty($property);
        if($value !== null) {
            if($property === bgSequence::ANNOTATION_EC_NUMBER) {
                if($value instanceof bgECNumber) {
                    return $value->__toString();
                }
            } elseif($property === bgSequence::ANNOTATION_ORGANISM_CLASSIFICATION) {
                if(!empty($value)) {
                    $doc = new DOMDocument();
                    $lineageNode = $doc->createElement('lineage');
                    foreach($value as $taxon) {
                        $lineageNode->appendChild($doc->createElement('taxon', $taxon));
                    }
                    return $lineageNode;
                }
            } else {
                $value = trim((string) $value);
                if($value !== '') {
                    return $value;
                }
            }
        }

        return null;
    }

    /**
     *
     * @param bgAlignment $alignment
     * @param string $filename file to write to
     * @throws bgIOException
     */
    public function write(bgAlignment $alignment, $filename) {
        $fh = @fopen($filename, 'w');
        if($fh === false) {
            throw new bgIOException($filename);
        }
        $this->writeToRessource($alignment, $fh);
        if(!fclose($fh)) {
            throw new bgIOException($filename);
        }
    }

    /**
     *
     * @param bgAlignment $alignment
     * @param resource $fh file or stream resource to write to
     * @throws bgIOException
     */
    public function writeToRessource(bgAlignment $alignment, $fh) {
        $type = get_resource_type($fh);
        if(!($type == 'file' || $type == 'stream')) {
            throw new bgIllegalArgumentException('$fh');
        }
        if(fwrite($fh, $this->toMACSIMS($alignment)) === false) {
            throw new bgIOException();
        }
    }

    /**
     *
     * @param bgAlignment $alignment
     * @return string
     */
    public function toMACSIMS(bgAlignment $alignment) {
        $doc = new DOMDocument('1.0', 'UTF-8');
        $doc->formatOutput = true;

        $dtd = $doc->implementation->createDocumentType(self::DTD_TYPE, null, self::DTD_SYSTEM_ID);
        $doc->appendChild($dtd);

        $root = $doc->createElement(self::DTD_TYPE);
        $doc->appendChild($root);

        $alignmentNode = $doc->createElement('alignment');
        $root->appendChild($alignmentNode);

        // set the required name to random id
        $alignmentNode->appendChild($doc->createElement('aln-name', uniqid(rand(), true)));

        // now create sequences
        foreach($alignment as $sequence) {
            // basic sequence creation
            $sequenceNode = $doc->createElement('sequence');

            if($sequence->getType() == bgSequence::TYPE_NUCLEIC) {
                $sequenceNode->setAttribute('seq-type', 'DNA');
            } elseif($sequence->getType() == bgSequence::TYPE_PROTEIN) {
                $sequenceNode->setAttribute('seq-type', 'Protein');
            } else {
                $sequenceNode->setAttribute('seq-type', 'PDB');
            }

            $sequenceNode->appendChild($doc->createElement('seq-name', $sequence->getName()));
            $sequenceNode->appendChild($doc->createElement('seq-data', $sequence->getSeqString()));

            // supported sequence information retrieving
            $seqInfo['accession'] = self::getAnnotation($sequence, bgSequence::ANNOTATION_ACCESSION);
            $seqInfo['definition'] = self::getAnnotation($sequence, bgSequence::ANNOTATION_DESCRIPTION);
            $seqInfo['organism'] = self::getAnnotation($sequence, bgSequence::ANNOTATION_ORGANISM);
            $seqInfo['ec'] = self::getAnnotation($sequence, bgSequence::ANNOTATION_EC_NUMBER);
            $seqInfo['taxid'] = self::getAnnotation($sequence, bgSequence::ANNOTATION_NCBI_TAXID);
            $seqInfo['lineage'] = self::getAnnotation($sequence, bgSequence::ANNOTATION_ORGANISM_CLASSIFICATION);
            $seqInfo['lifedomain'] = self::getAnnotation($sequence, self::ANNOTATION_LIFE_DOMAIN);
            $seqInfo['hydrophobicity'] = self::getAnnotation($sequence, self::ANNOTATION_HYDROPHOBICITY);

            // append seq-info
            $seqInfoNode = null;
            foreach($seqInfo as $element => $value) {
                if($value !== null) {
                    if($seqInfoNode === null) {
                        $seqInfoNode = $doc->createElement('seq-info');
                        $sequenceNode->appendChild($seqInfoNode);
                    }
                    if($value instanceof DOMElement) {
                        $toto = $doc->importNode($value, true);
                        $seqInfoNode->appendChild($toto);
                    } else {
                        $seqInfoNode->appendChild($doc->createElement($element, $value));
                    }
                }
            }
            $ftableNode = null;
            foreach($sequence->features() as $feature) {
                // append level 1 features only
                if($seqInfoNode === null) {
                    $seqInfoNode = $doc->createElement('seq-info');
                    $sequenceNode->appendChild($seqInfoNode);
                }
                if($ftableNode === null) {
                    $ftableNode = $doc->createElement('ftable');
                    $seqInfoNode->appendChild($ftableNode);
                }
                $fitemNode = $doc->createElement('fitem');
                $ftableNode->appendChild($fitemNode);

                $fitemNode->appendChild($doc->createElement('ftype', $feature->getType()));
                $fitemNode->appendChild($doc->createElement('fstart', $feature->getLocation()->getMin()));
                $fitemNode->appendChild($doc->createElement('fstop', $feature->getLocation()->getMax()));
                if(($fcolor = $feature->getAnnotation()->getProperty(self::ANNOTATION_FEATURE_COLOR)) !== null) {
                    $fitemNode->appendChild($doc->createElement('fcolor', $fcolor));
                } else {
                    $fitemNode->appendChild($doc->createElement('fcolor', 0)); // FIXME: set color to 0 by default
                }
                if(($fscore = $feature->getAnnotation()->getProperty(self::ANNOTATION_FEATURE_SCORE)) !== null) {
                    $fitemNode->appendChild($doc->createElement('fscore', $fscore));
                }
                if(($fnote = $feature->getAnnotation()->getProperty(self::ANNOTATION_FEATURE_NOTE)) !== null) {
                    $fitemNode->appendChild($doc->createElement('fnote', $fnote));
                }
            }

            // append sequence
            $alignmentNode->appendChild($sequenceNode);
        }

        return $doc->saveXML();
    }

    private static function initPalette() {
        if(self::$colorPalette === null) {
            $colors = array(
                'rgb(0, 0, 225)',
                'rgb(192, 192, 0)',
                'rgb(128, 0, 0)',
                'rgb(0, 225, 225)',
                'rgb(0, 128, 128)',
                'rgb(64, 128, 0)',
                'rgb(192, 128, 0)',
                'rgb(192, 128, 192)',
                'rgb(0, 150, 192)',
                'rgb(64, 64, 225)',
                'rgb(192, 225, 0)',
                'rgb(255, 192, 0)',
                'rgb(225, 0, 128)',
                'rgb(0, 225, 192)',
                'rgb(150, 0, 225)',
                'rgb(0, 192, 225)',
                'rgb(164, 225, 64)',
                'rgb(225, 192, 64)',
                'rgb(225, 64, 192)',
                'rgb(64, 64, 225)',
                'rgb(192, 64, 225)',
                'rgb(192, 225, 64)',
                'rgb(128, 0, 128)',
                'rgb(192, 0, 192)'
            );

            $cp = new bgColorPalette();
            foreach($colors as $key => $value) {
                $cp->addColor($key, bgColor::decode($value));
            }
            self::$colorPalette = $cp;
            self::$availableColors = count($colors);
        }
    }


    private static function getColor($index) {
        $index = (int) $index;

        self::initPalette();

        return self::$colorPalette->getColor($index % self::$availableColors);
    }
}
?>