/****************************************************************************
 * Copyright (C) 2009-2010 GGA Software Services LLC
 *
 * This file may be distributed and/or modified under the terms of the
 * GNU Affero General Public License version 3 as published by the Free
 * Software Foundation and appearing in the file LICENSE.GPL included in
 * the packaging of this file.
 *
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 ***************************************************************************/

if (!window.chem || !util.Vec2 || !util.Pool)
    throw new Error("Vec2, Pool should be defined first");
chem.SGroup = function (type)
{
    if (!type || !(type in chem.SGroup.TYPES))
        throw new Error("Invalid or unsupported s-group type");

    this.type = type;
    this.id = -1;
    chem.SGroup.equip(this, type);
    this.label = -1;
    this.bracketBox = null;
        this.bracketDir = new util.Vec2(1,0);
        this.areas = [];

    this.highlight = false;
    this.highlighting = null;
    this.selected = false;
    this.selectionPlate = null;

    this.atoms = [];
    this.patoms = [];
    this.bonds = [];
    this.xBonds = [];
    this.neiAtoms = [];
    this.pp = null;
    this.data = {
        'mul': 1, // multiplication count for MUL group
        'connectivity': 'ht', // head-to-head, head-to-tail or either-unknown
        'name' : '',
        'subscript' : 'n',

        // data s-group fields
        'attached' : false,
        'absolute' : true,
        'showUnits' : false,
        'nCharsToDisplay' : -1,
        'tagChar' : '',
        'daspPos' : 1,
        'fieldType' : 'F',
        'fieldName' : '',
        'fieldValue' : '',
        'units' : '',
        'query' : '',
        'queryOp' : ''
    }
};

// TODO: these methods should be overridden
//      and should only accept valid attributes for each S-group type.
//      The attributes should be accessed via these methods only and not directly through this.data.
// stub
chem.SGroup.prototype.getAttr = function (attr) {
    return this.data[attr];
};

// TODO: should be group-specific
chem.SGroup.prototype.getAttrs = function () {
    var attrs = {};
    for (var attr in this.data)
        attrs[attr] = this.data[attr];
    return attrs;
};

// stub
chem.SGroup.prototype.setAttr = function (attr, value) {
    var oldValue = this.data[attr];
    this.data[attr] = value;
    return oldValue;
};

// stub
chem.SGroup.prototype.checkAttr = function (attr, value) {
    return this.data[attr] == value;
};

chem.SGroup.equip = function (sgroup, type) {
    var impl = chem.SGroup.TYPES[type];
    for (var method in impl)
        sgroup[method] = impl[method];
};

chem.SGroup.numberArrayToString = function (numbers, map) {
    var str = util.stringPadded(numbers.length, 3);
    for (var i = 0; i < numbers.length; ++i) {
        str += ' ' + util.stringPadded(map[numbers[i]], 3);
    }
    return str;
};

chem.SGroup.addGroup = function (mol, sg, atomMap)
{
    // add the group to the molecule
    sg.id = mol.sgroups.add(sg);

    // apply type-specific post-processing
    sg.postLoad(mol, atomMap);

    // mark atoms in the group as belonging to it
    for (var s = 0; s < sg.atoms.length; ++s)
        if (mol.atoms.has(sg.atoms[s]))
            util.Set.add(mol.atoms.get(sg.atoms[s]).sgs, sg.id);

    mol.sGroupForest.insert(sg.id);
    return sg.id;
};

chem.SGroup.bracketsToMolfile = function (mol, sg, idstr) {
    var inBonds = [], xBonds = [];
    var atomSet = util.Set.fromList(sg.atoms);
    chem.SGroup.getCrossBonds(inBonds, xBonds, mol, atomSet);
    chem.SGroup.bracketPos(sg, null, mol, xBonds);
    var bb = sg.bracketBox;
    var d = sg.bracketDir, n = d.rotateSC(1, 0);
    var brackets = chem.SGroup.getBracketParameters(mol, xBonds, atomSet, bb, d, n, null, sg.id);
    var lines = [];
    for (var i = 0; i < brackets.length; ++i) {
        var bracket = brackets[i];
        var a0 = bracket.c.addScaled(bracket.n, -0.5 * bracket.h).yComplement();
        var a1 = bracket.c.addScaled(bracket.n, 0.5 * bracket.h).yComplement();
        var line = 'M  SDI ' + idstr + util.paddedInt(4, 3);
        var coord = [a0.x, a0.y, a1.x, a1.y];
        for (var j = 0; j < coord.length; ++j) {
            line += util.paddedFloat(coord[j], 10, 4);
        }
        lines.push(line);
    }
    return lines;
};

chem.SGroup.filterAtoms = function (atoms, map) {
    var newAtoms = [];
    for (var i = 0; i < atoms.length; ++i) {
        var aid = atoms[i];
        if (typeof(map[aid]) != "number") {
            newAtoms.push(aid);
        } else if (map[aid] >= 0) {
            newAtoms.push(map[aid]);
        } else {
            newAtoms.push(-1);
        }
    }
    return newAtoms;
};

chem.SGroup.removeNegative = function (atoms) {
    var newAtoms = [];
    for (var j = 0; j < atoms.length; ++j)
        if (atoms[j] >= 0)
            newAtoms.push(atoms[j]);
    return newAtoms;
};

chem.SGroup.filter = function (mol, sg, atomMap)
{
    sg.atoms = chem.SGroup.removeNegative(chem.SGroup.filterAtoms(sg.atoms, atomMap));
};

chem.SGroup.clone = function (sgroup, aidMap, bidMap)
{
    var cp = new chem.SGroup(sgroup.type);

    for (var field in sgroup.data) { // TODO: remove all non-primitive properties from 'data'
        cp.data[field] = sgroup.data[field];
    }
    cp.atoms = util.mapArray(sgroup.atoms, aidMap);
    cp.pp = sgroup.pp;
    cp.bracketBox = sgroup.bracketBox;
    cp.patoms = null;
    cp.bonds = null;
    cp.allAtoms = sgroup.allAtoms;
    return cp;
};

chem.SGroup.addAtom = function (sgroup, aid)
{
    sgroup.atoms.push(aid);
};

chem.SGroup.removeAtom = function (sgroup, aid)
{
    for (var i = 0; i < sgroup.atoms.length; ++i) {
        if (sgroup.atoms[i] === aid) {
            sgroup.atoms.splice(i, 1);
            return;
        }
    }
    throw new Error("The atom is not found in the given s-group");
};

chem.SGroup.getCrossBonds = function (inBonds, xBonds, mol, parentAtomSet) {
    mol.bonds.each(function(bid, bond){
        if (util.Set.contains(parentAtomSet, bond.begin) && util.Set.contains(parentAtomSet, bond.end)) {
            if (!util.isNull(inBonds))
                inBonds.push(bid);
        } else if (util.Set.contains(parentAtomSet, bond.begin) || util.Set.contains(parentAtomSet, bond.end)) {
            if (!util.isNull(xBonds))
                xBonds.push(bid);
        }
    }, this);
};

chem.SGroup.bracketPos = function(sg, render, mol, xbonds) {
    var atoms = sg.atoms;
    if (!xbonds || xbonds.length !== 2) {
        sg.bracketDir = new util.Vec2(1, 0);
    } else {
        var b1 = mol.bonds.get(xbonds[0]), b2 = mol.bonds.get(xbonds[1]);
        var p1 = b1.getCenter(mol), p2 = b2.getCenter(mol);
        sg.bracketDir = util.Vec2.diff(p2, p1).normalized();
    }
    var d = sg.bracketDir;
    var n = d.rotateSC(1, 0);

    var bb = null;
    var contentBoxes = [];
    util.each(atoms, function(aid) {
        var atom = mol.atoms.get(aid);
        var bba = render ? render.ctab.atoms.get(aid).visel.boundingBox : null;
        var pos = new util.Vec2(atom.pp);
        if (util.isNull(bba)) {
            bba = new util.Box2Abs(pos, pos);
            var ext = new util.Vec2(0.05 * 3, 0.05 * 3);
            bba = bba.extend(ext, ext);
        } else {
            bba = bba.translate((render.offset || new util.Vec2()).negated()).transform(render.scaled2obj, render);
        }
        contentBoxes.push(bba);
    }, this);
    util.each(mol.sGroupForest.children.get(sg.id), function(sgid) {
        var bba = render ? render.ctab.sgroups.get(sgid).visel.boundingBox : null;
        if (util.isNull(bba))
            return; // TODO: use object box instead
        bba = bba.translate((render.offset || new util.Vec2()).negated()).transform(render.scaled2obj, render);
        contentBoxes.push(bba);
    }, this);
    util.each(contentBoxes, function(bba) {
        var bbb = null;
        util.each([bba.p0.x, bba.p1.x], function(x) {
            util.each([bba.p0.y, bba.p1.y], function(y) {
                var v = new util.Vec2(x, y);
                var p = new util.Vec2(util.Vec2.dot(v, d), util.Vec2.dot(v, n));
                bbb = util.isNull(bbb) ? new util.Box2Abs(p, p) : bbb.include(p);
            }, this);
        }, this);
        bb = util.isNull(bb) ? bbb : util.Box2Abs.union(bb, bbb);
    }, this);
    var vext = new util.Vec2(0.2, 0.4);
    if (!util.isNull(bb))
        bb = bb.extend(vext, vext);
    sg.bracketBox = bb;
};

chem.SGroup.drawBrackets = function (set, render, sg, xbonds, atomSet, bb, d, n, lowerIndexText, upperIndexText, indexAttribute) {
    var brackets = chem.SGroup.getBracketParameters(render.ctab.molecule, xbonds, atomSet, bb, d, n, render, sg.id);
    var ir = -1;
    for (var i = 0; i < brackets.length; ++i) {
       var bracket = brackets[i];
       var path = chem.SGroup.drawBracket(render, render.paper, render.styles, bracket.d, bracket.n, bracket.c, bracket.w, bracket.h);
       set.push(path);
       if (ir < 0 || brackets[ir].d.x < bracket.d.x || (brackets[ir].d.x == bracket.d.x && brackets[ir].d.y > bracket.d.y))
           ir = i;
    }
    var bracketR = brackets[ir];
    var renderIndex = function(text, shift) {
        var indexPos = render.ps(bracketR.c.addScaled(bracketR.n, shift * bracketR.h));
        var indexPath = render.paper.text(indexPos.x, indexPos.y, text)
        .attr({
            'font' : render.settings.font,
            'font-size' : render.settings.fontszsub
        });
        if (indexAttribute)
            indexPath.attr(indexAttribute);
        var indexBox = util.Box2Abs.fromRelBox(rnd.relBox(indexPath.getBBox()));
        var t = Math.max(util.Vec2.shiftRayBox(indexPos, bracketR.d.negated(), indexBox), 3) + 2;
        indexPath.translateAbs(t * bracketR.d.x, t * bracketR.d.y);
        set.push(indexPath);
    }
    if (lowerIndexText) {
        renderIndex(lowerIndexText, 0.5);
    }
    if (upperIndexText) {
        renderIndex(upperIndexText, -0.5);
    }
};

chem.SGroup.drawBracket = function (render, paper, styles, d, n, c, bracketWidth, bracketHeight) {
    bracketWidth = bracketWidth || 0.25;
    bracketHeight = bracketHeight || 1.0;
    var a0 = c.addScaled(n, -0.5*bracketHeight);
    var a1 = c.addScaled(n, 0.5*bracketHeight);
    var b0 = a0.addScaled(d, -bracketWidth);
    var b1 = a1.addScaled(d, -bracketWidth);

    a0 = render.obj2scaled(a0);
    a1 = render.obj2scaled(a1);
    b0 = render.obj2scaled(b0);
    b1 = render.obj2scaled(b1);

    return paper.path("M {0}, {1} L {2} , {3} L {4} , {5} L {6} , {7}",
        b0.x, b0.y, a0.x, a0.y, a1.x, a1.y, b1.x, b1.y)
        .attr(styles.sgroupBracketStyle);
}

chem.SGroup.getBracketParameters = function (mol, xbonds, atomSet, bb, d, n, render, id) {
    var bracketParams = function(c, d, w, h) {
        this.c = c;
        this.d = d;
        this.n = d.rotateSC(1,0);
        this.w = w;
        this.h = h;
    }
    var brackets = [];
    if (xbonds.length < 2) {
        (function () {
            d = d || new util.Vec2(1, 0);
            n = n || d.rotateSC(1, 0);
            var bracketWidth = Math.min(0.25, bb.sz().x * 0.3);
            var cl = util.Vec2.lc2(d, bb.p0.x, n, 0.5*(bb.p0.y+bb.p1.y));
            var cr = util.Vec2.lc2(d, bb.p1.x, n, 0.5*(bb.p0.y+bb.p1.y));
            var bracketHeight = bb.sz().y;

            brackets.push(new bracketParams(cl, d.negated(), bracketWidth, bracketHeight), new bracketParams(cr, d, bracketWidth, bracketHeight));
        })();
    } else if (xbonds.length === 2) {
        (function () {
            var b1 = mol.bonds.get(xbonds[0]), b2 = mol.bonds.get(xbonds[1]);
            var cl0 = b1.getCenter(mol), cr0 = b2.getCenter(mol), tl = -1, tr = -1, tt = -1, tb = -1, cc = util.Vec2.centre(cl0, cr0);
            var dr = util.Vec2.diff(cr0, cl0).normalized(), dl = dr.negated(), dt = dr.rotateSC(1,0), db = dt.negated();
            
            util.each(mol.sGroupForest.children.get(id), function(sgid) {
                var bba = render ? render.ctab.sgroups.get(sgid).visel.boundingBox : null;
                if (util.isNull(bba))
                    return; // TODO: use object box instead
                bba = bba.translate((render.offset || new util.Vec2()).negated()).transform(render.scaled2obj, render);
                tl = Math.max(tl, util.Vec2.shiftRayBox(cl0, dl, bba));
                tr = Math.max(tr, util.Vec2.shiftRayBox(cr0, dr, bba));
                tt = Math.max(tt, util.Vec2.shiftRayBox(cc, dt, bba));
                tb = Math.max(tb, util.Vec2.shiftRayBox(cc, db, bba));
            }, this);
            tl = Math.max(tl + 0.2, 0);
            tr = Math.max(tr + 0.2, 0);
            tt = Math.max(Math.max(tt, tb) + 0.1, 0);
            var bracketWidth = 0.25, bracketHeight = 1.5 + tt;
            brackets.push(new bracketParams(cl0.addScaled(dl, tl), dl, bracketWidth, bracketHeight),
            new bracketParams(cr0.addScaled(dr, tr), dr, bracketWidth, bracketHeight));
        })();

    } else {
        (function () {
            for (var i = 0; i < xbonds.length; ++i) {
                var b = mol.bonds.get(xbonds[i]);
                var c = b.getCenter(mol);
                var d = util.Set.contains(atomSet, b.begin) ? b.getDir(mol) : b.getDir(mol).negated();
                brackets.push(new bracketParams(c, d, 0.2, 1.0));
            }
        })();
    }
    return brackets;
};

chem.SGroup.getObjBBox = function (atoms, mol)
{
    if (atoms.length == 0)
        throw new Error("Atom list is empty");

    var a0 = mol.atoms.get(atoms[0]).pp;
    var bb = new util.Box2Abs(a0, a0);
    for (var i = 1; i < atoms.length; ++i) {
        var aid = atoms[i];
        var atom = mol.atoms.get(aid);
        var p = atom.pp;
        bb = bb.include(p);
    }
    return bb;
};

chem.SGroup.makeAtomBondLines = function (prefix, idstr, ids, map) {
    if (!ids)
        return [];
    var lines = [];
    for (var i = 0; i < Math.floor((ids.length + 14) / 15); ++i) {
        var rem = Math.min(ids.length - 15 * i, 15);
        var salLine = 'M  ' + prefix + ' ' + idstr + ' ' + util.paddedInt(rem, 2);
        for (var j = 0; j < rem; ++j) {
            salLine += ' ' + util.paddedInt(map[ids[i * 15 + j]], 3);
        }
        lines.push(salLine);
    }
    return lines;
};

chem.SGroup.getAtoms = function (mol, sg) {
    if (!sg.allAtoms)
        return sg.atoms;
    var atoms = [];
    mol.atoms.each(function(aid){
        atoms.push(aid);
    });
    return atoms;
};

chem.SGroup.getBonds = function (mol, sg) {
    var atoms = chem.SGroup.getAtoms(mol, sg);
    var bonds = [];
    mol.bonds.each(function(bid, bond){
        if (atoms.indexOf(bond.begin) >= 0 && atoms.indexOf(bond.end) >= 0) bonds.push(bid);
    });
    return bonds;
};

chem.SGroup.GroupMul = {
    draw: function (remol) {
            var render = remol.render;
            var set = render.paper.set();
            var inBonds = [], xBonds = [];
            var atomSet = util.Set.fromList(this.atoms);
            chem.SGroup.getCrossBonds(inBonds, xBonds, remol.molecule, atomSet);
            chem.SGroup.bracketPos(this, render, remol.molecule, xBonds);
            var bb = this.bracketBox;
            var d = this.bracketDir, n = d.rotateSC(1, 0);
            this.areas = [bb];
            chem.SGroup.drawBrackets(set, render, this, xBonds, atomSet, bb, d, n, this.data.mul);
            return set;
    },

    saveToMolfile: function (mol, sgMap, atomMap, bondMap) {
        var idstr = util.stringPadded(sgMap[this.id], 3);

        var lines = [];
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SAL', idstr, util.idList(this.atomSet), atomMap)); // TODO: check atomSet
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SPA', idstr, util.idList(this.parentAtomSet), atomMap));
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SBL', idstr, this.bonds, bondMap));
        var smtLine = 'M  SMT ' + idstr + ' ' + this.data.mul;
        lines.push(smtLine);
        lines = lines.concat(chem.SGroup.bracketsToMolfile(mol, this, idstr));
        return lines.join('\n');
    },

    prepareForSaving: function (mol) {
            var j;
            this.atoms.sort();
            this.atomSet = util.Set.fromList(this.atoms);
            this.parentAtomSet = util.Set.clone(this.atomSet);
            var inBonds = [];
            var xBonds = [];

            mol.bonds.each(function(bid, bond){
                if (util.Set.contains(this.parentAtomSet, bond.begin) && util.Set.contains(this.parentAtomSet, bond.end))
                    inBonds.push(bid);
                else if (util.Set.contains(this.parentAtomSet, bond.begin) || util.Set.contains(this.parentAtomSet,bond.end))
                    xBonds.push(bid);
            }, this);
            if (xBonds.length != 0 && xBonds.length != 2)
                throw {
                    'id':this.id,
                    'error-type':'cross-bond-number',
                    'message':"Unsupported cross-bonds number"
                };

            var xAtom1 = -1,
            xAtom2 = -1;
            var crossBond = null;
            if (xBonds.length == 2) {
                var bond1 = mol.bonds.get(xBonds[0]);
                if (util.Set.contains(this.parentAtomSet, bond1.begin)) {
                    xAtom1 = bond1.begin;
                } else {
                    xAtom1 = bond1.end;
                }
                var bond2 = mol.bonds.get(xBonds[1]);
                if (util.Set.contains(this.parentAtomSet, bond2.begin)) {
                    xAtom2 = bond2.begin;
                } else {
                    xAtom2 = bond2.end;
                }
                crossBond = bond2;
            }

            var amap = null;
            var tailAtom = xAtom1;

            var newAtoms = [];
            for (j = 0; j < this.data.mul - 1; ++j) {
                amap = {};
                util.each(this.atoms, function(aid) {
                    var atom = mol.atoms.get(aid);
                    var aid2 = mol.atoms.add(new chem.Struct.Atom(atom));
                    newAtoms.push(aid2);
                    this.atomSet[aid2] = 1;
                    amap[aid] = aid2;
                }, this);
                util.each(inBonds, function(bid) {
                    var bond = mol.bonds.get(bid);
                    var newBond = new chem.Struct.Bond(bond);
                    newBond.begin = amap[newBond.begin];
                    newBond.end = amap[newBond.end];
                    mol.bonds.add(newBond);
                }, this);
                if (crossBond != null) {
                    var newCrossBond = new chem.Struct.Bond(crossBond);
                    newCrossBond.begin = tailAtom;
                    newCrossBond.end = amap[xAtom2];
                    mol.bonds.add(newCrossBond);
                    tailAtom = amap[xAtom1];
                }
            }
            
            util.each(newAtoms, function(aid) {
                util.each(mol.sGroupForest.getPathToRoot(this.id).reverse(), function(sgid) {
                    mol.atomAddToSGroup(sgid, aid);
                }, this);
            }, this);
            if (tailAtom >= 0) {
                var xBond2 = mol.bonds.get(xBonds[0]);
                if (xBond2.begin == xAtom1)
                    xBond2.begin = tailAtom;
                else
                    xBond2.end = tailAtom;
            }

            this.bonds = xBonds;
    },

    postLoad: function (mol, atomMap)
    {
        this.data.mul = this.data.subscript - 0;
        var atomReductionMap = {};

        this.atoms = chem.SGroup.filterAtoms(this.atoms, atomMap);
        this.patoms = chem.SGroup.filterAtoms(this.patoms, atomMap);

        // mark repetitions for removal
        for (var k = 1; k < this.data.mul; ++k) {
            for (var m = 0; m < this.patoms.length; ++m) {
                var raid = this.atoms[k * this.patoms.length + m];
                if (raid < 0)
                    continue;
                if (this.patoms[m] < 0) {
                    throw new Error("parent atom missing");
                }
//                mol.atoms.get(raid).pp.y -= 3*k; // for debugging purposes
                atomReductionMap[raid] = this.patoms[m]; // "merge" atom in parent
            }
        }
        this.patoms = chem.SGroup.removeNegative(this.patoms);

        var patomsMap = util.identityMap(this.patoms);

        var bondsToRemove = [];
        mol.bonds.each(function(bid, bond){
            var beginIn = bond.begin in atomReductionMap;
            var endIn = bond.end in atomReductionMap;
            // if both adjacent atoms of a bond are to be merged, remove it
            if (beginIn && endIn
                || beginIn && bond.end in patomsMap
                || endIn && bond.begin in patomsMap) {
                bondsToRemove.push(bid);
            // if just one atom is merged, modify the bond accordingly
            } else if (beginIn) {
                bond.begin = atomReductionMap[bond.begin];
            } else if (endIn) {
                bond.end = atomReductionMap[bond.end];
            }
        }, this);

        // apply removal lists
        for (var b = 0; b < bondsToRemove.length; ++b) {
            mol.bonds.remove(bondsToRemove[b]);
        }
        for (var a in atomReductionMap) {
            mol.atoms.remove(a);
            atomMap[a] = -1;
        }
        this.atoms = this.patoms;
        this.patoms = null;
    }
};

chem.SGroup.GroupSru = {
    draw: function (remol) {
            var render = remol.render;
            var set = render.paper.set();
            var inBonds = [], xBonds = [];
            var atomSet = util.Set.fromList(this.atoms);
            chem.SGroup.getCrossBonds(inBonds, xBonds, remol.molecule, atomSet);
            chem.SGroup.bracketPos(this, render, remol.molecule, xBonds);
            var bb = this.bracketBox;
            var d = this.bracketDir, n = d.rotateSC(1, 0);
            this.areas = [bb];
            var connectivity = this.data.connectivity || 'eu';
            if (connectivity == 'ht')
                connectivity = '';
            var subscript = this.data.subscript || 'n';
            chem.SGroup.drawBrackets(set, render, this, xBonds, atomSet, bb, d, n, subscript, connectivity);
            return set;
    },

    saveToMolfile: function (mol, sgMap, atomMap, bondMap) {
        var idstr = util.stringPadded(sgMap[this.id], 3);

        var lines = [];
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SAL', idstr, this.atoms, atomMap));
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SBL', idstr, this.bonds, bondMap));
        lines = lines.concat(chem.SGroup.bracketsToMolfile(mol, this, idstr));
        return lines.join('\n');
    },

    prepareForSaving: function (mol) {
        var xBonds = [];
        mol.bonds.each(function(bid, bond){
            var a1 = mol.atoms.get(bond.begin);
            var a2 = mol.atoms.get(bond.end);
            if (util.Set.contains(a1.sgs, this.id) && !util.Set.contains(a2.sgs, this.id) ||
                util.Set.contains(a2.sgs, this.id) && !util.Set.contains(a1.sgs, this.id))
                xBonds.push(bid);
        },this);
        if (xBonds.length != 0 && xBonds.length != 2)
            throw {'id':this.id, 'error-type':'cross-bond-number', 'message':"Unsupported cross-bonds number"};
        this.bonds = xBonds;
    },

    postLoad: function (mol, atomMap) {
        this.data.connectivity = (this.data.connectivity || 'EU').strip().toLowerCase();
    }
};

chem.SGroup.GroupSup = {
    draw: function (remol) {
            var render = remol.render;
            var set = render.paper.set();
            var inBonds = [], xBonds = [];
            var atomSet = util.Set.fromList(this.atoms);
            chem.SGroup.getCrossBonds(inBonds, xBonds, remol.molecule, atomSet);
            chem.SGroup.bracketPos(this, render, remol.molecule, xBonds);
            var bb = this.bracketBox;
            var d = this.bracketDir, n = d.rotateSC(1, 0);
            this.areas = [bb];
            chem.SGroup.drawBrackets(set, render, this, xBonds, atomSet, bb, d, n, this.data.name, null, {
                'font-style' : 'italic'
            });
            return set;
    },

    saveToMolfile: function (mol, sgMap, atomMap, bondMap) {
        var idstr = util.stringPadded(sgMap[this.id], 3);

        var lines = [];
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SAL', idstr, this.atoms, atomMap));
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SBL', idstr, this.bonds, bondMap));
        if (this.data.name && this.data.name != '')
            lines.push('M  SMT ' + idstr + ' ' + this.data.name);
        return lines.join('\n');
    },

    prepareForSaving: function (mol) {
        // This code is also used for GroupSru and should be moved into a separate common method
        // It seems that such code should be used for any sgroup by this this should be checked
        var xBonds = [];
        mol.bonds.each(function(bid, bond){
            var a1 = mol.atoms.get(bond.begin);
            var a2 = mol.atoms.get(bond.end);
            if (util.Set.contains(a1.sgs, this.id) && !util.Set.contains(a2.sgs, this.id) ||
                util.Set.contains(a2.sgs, this.id) && !util.Set.contains(a1.sgs, this.id))
                xBonds.push(bid);
        },this);
        this.bonds = xBonds;
    },

    postLoad: function (mol, atomMap) {
        this.data.name = (this.data.subscript || '').strip();
                this.data.subscript = '';
    }
};

chem.SGroup.GroupGen = {
        draw: function (remol) {
            var render = remol.render;
            var settings = render.settings;
            var styles = render.styles;
            var paper = render.paper;
            var set = paper.set();
            var inBonds = [], xBonds = [];
            var atomSet = util.Set.fromList(this.atoms);
            chem.SGroup.getCrossBonds(inBonds, xBonds, remol.molecule, atomSet);
            chem.SGroup.bracketPos(this, render, remol.molecule, xBonds);
            var bb = this.bracketBox;
            var d = this.bracketDir, n = d.rotateSC(1, 0);
            this.areas = [bb];
            chem.SGroup.drawBrackets(set, render, this, xBonds, atomSet, bb, d, n);
            return set;
    },

    saveToMolfile: function (mol, sgMap, atomMap, bondMap) {
        var idstr = util.stringPadded(sgMap[this.id], 3);

        var lines = [];
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SAL', idstr, this.atoms, atomMap));
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SBL', idstr, this.bonds, bondMap));
        lines = lines.concat(chem.SGroup.bracketsToMolfile(mol, this, idstr));
        return lines.join('\n');
    },

    prepareForSaving: function (mol) {
    },

    postLoad: function (mol, atomMap) {
    }
};

chem.SGroup.getMassCentre = function (mol, atoms) {
    var c = new util.Vec2(); // mass centre
    for (var i = 0; i < atoms.length; ++i) {
        c = c.addScaled(mol.atoms.get(atoms[i]).pp, 1.0 / atoms.length);
    }
    return c;
};

chem.SGroup.setPos = function (remol, sg, pos) {
    sg.pp = pos;
};

chem.SGroup.GroupDat = {
    showValue: function (paper, pos, sg, settings) {
        var name = paper.text(pos.x, pos.y, sg.data.fieldValue)
        .attr({
            'font' : settings.font,
            'font-size' : settings.fontsz
        });
        return name;
    },

    draw: function (remol) {
        var render = remol.render;
        var settings = render.settings;
        var paper = render.paper;
        var set = paper.set();
        var atoms = chem.SGroup.getAtoms(remol, this);
        var i;
        chem.SGroup.bracketPos(this, render, remol.molecule);
        this.areas = this.bracketBox ? [this.bracketBox] : [];
        if (this.pp == null) {
            // NB: we did not pass xbonds parameter to the backetPos method above,
            //  so the result will be in the regular coordinate system
            chem.SGroup.setPos(remol, this, this.bracketBox.p1.add(new util.Vec2(0.5, 0.5)));
        }
        var ps = this.pp.scaled(settings.scaleFactor);

        if (this.data.attached) {
                for (i = 0; i < atoms.length; ++i) {
                var atom = remol.atoms.get(atoms[i]);
                var p = render.ps(atom.a.pp);
                var bb = atom.visel.boundingBox;
                if (bb != null) {
                    p.x = Math.max(p.x, bb.p1.x);
                }
                p.x += settings.lineWidth; // shift a bit to the right
                var name_i = this.showValue(paper, p, this, settings);
                var box_i = rnd.relBox(name_i.getBBox());
                name_i.translateAbs(0.5 * box_i.width, -0.3 * box_i.height);
                set.push(name_i);
                var sbox_i = util.Box2Abs.fromRelBox(rnd.relBox(name_i.getBBox()));
                sbox_i = sbox_i.transform(render.scaled2obj, render);
                this.areas.push(sbox_i);
            }
        } else {
            var name = this.showValue(paper, ps, this, settings);
            var box = rnd.relBox(name.getBBox());
            name.translateAbs(0.5 * box.width, -0.5 * box.height);
            set.push(name);
            var sbox = util.Box2Abs.fromRelBox(rnd.relBox(name.getBBox()));
            this.dataArea = sbox.transform(render.scaled2obj, render);
            if (!remol.sgroupData.has(this.id))
                remol.sgroupData.set(this.id, new rnd.ReDataSGroupData(this));
        }
        return set;
    },

    saveToMolfile: function (mol, sgMap, atomMap, bondMap) {
        var idstr = util.stringPadded(sgMap[this.id], 3);

        var data = this.data;
        var pp = this.pp;
        if (!data.absolute)
            pp = pp.sub(chem.SGroup.getMassCentre(mol, this.atoms));
        var lines = [];
        lines = lines.concat(chem.SGroup.makeAtomBondLines('SAL', idstr, this.atoms, atomMap));
        var sdtLine = 'M  SDT ' + idstr +
        ' ' + util.stringPadded(data.fieldName, 30, true) +
        util.stringPadded(data.fieldType, 2) +
        util.stringPadded(data.units, 20, true) +
        util.stringPadded(data.query, 2) +
        util.stringPadded(data.queryOp, 3);
        lines.push(sdtLine);
        var sddLine = 'M  SDD ' + idstr +
        ' ' + util.paddedFloat(pp.x, 10, 4) + util.paddedFloat(-pp.y, 10, 4) +
        '    ' + // ' eee'
        (data.attached ? 'A' : 'D') + // f
        (data.absolute ? 'A' : 'R') + // g
        (data.showUnits ? 'U' : ' ') + // h
        '   ' + //  i
        (data.nCharnCharsToDisplay >= 0 ? util.paddedInt(data.nCharnCharsToDisplay, 3) : 'ALL') + // jjj
        '  1   ' + // 'kkk ll '
        util.stringPadded(data.tagChar, 1) + // m
        '  ' + util.paddedInt(data.daspPos, 1) + // n
        '  '; // oo
        lines.push(sddLine);
        data.fieldValue.replace(/[\r\n|\n|\r]*$/, '').split(/\r\n|\n|\r/).each(function(str) {
            var charsPerLine = 69;
            while (str.length > charsPerLine) {
                lines.push('M  SCD ' + idstr + ' ' + str.slice(0, charsPerLine));
                str = str.slice(69);
            }
            lines.push('M  SED ' + idstr + ' ' + util.stringPadded(str, charsPerLine, true));
        }, this);
        return lines.join('\n');
    },

    prepareForSaving: function (mol) {
        this.atoms = chem.SGroup.getAtoms(mol, this);
    },

    postLoad: function (mol, atomMap) {
        var allAtomsInGroup = this.atoms.length == mol.atoms.count();
        if (!this.data.absolute)
            this.pp = this.pp.add(chem.SGroup.getMassCentre(mol, this.atoms));
        if (allAtomsInGroup &&
            (    this.data.fieldName == 'MDLBG_FRAGMENT_STEREO' ||
                this.data.fieldName == 'MDLBG_FRAGMENT_COEFFICIENT' ||
                this.data.fieldName == 'MDLBG_FRAGMENT_CHARGE')) {
            this.atoms = [];
            this.allAtoms = true;
        }
    }
};

chem.SGroup.TYPES = {
    'MUL': chem.SGroup.GroupMul,
    'SRU': chem.SGroup.GroupSru,
    'SUP': chem.SGroup.GroupSup,
    'DAT': chem.SGroup.GroupDat,
    'GEN': chem.SGroup.GroupGen
};


chem.SGroupForest = function (molecule) {
    this.parent = new util.Map(); // child id -> parent id
    this.children = new util.Map(); // parent id -> list of child ids
    this.children.set(-1, []); // extra root node
    this.molecule = molecule;
}

// returns an array or s-group ids in the order of breadth-first search
chem.SGroupForest.prototype.getSGroupsBFS = function() {
    var order = [], queue = [], id = -1;
    queue = util.array(this.children.get(-1));
    while (queue.length > 0) {
        var id = queue.shift();
        queue = queue.concat(this.children.get(id));
        order.push(id);
    }
    return order;
}

chem.SGroupForest.prototype.getAtomSets = function() {
    return this.molecule.sgroups.map(function(sgid, sgroup){
        return util.Set.fromList(sgroup.atoms);
    });
}

chem.SGroupForest.prototype.getAtomSetRelations = function(newId, atoms /* util.Set */, atomSets /* util.Map of util.Set */) {
    // find the lowest superset in the hierarchy
    var isStrictSuperset = new util.Map(), isSubset = new util.Map();
    var atomSets = this.getAtomSets();
    atomSets.unset(newId);
    atomSets.each(function(id, atomSet) {
       isSubset.set(id, util.Set.subset(atoms, atomSet));
       isStrictSuperset.set(id, util.Set.subset(atomSet, atoms) && !util.Set.eq(atomSet, atoms));
    }, this);
    var parents = atomSets.findAll(function(id) {
        if (!isSubset.get(id))
            return false;
        if (util.find(this.children.get(id), function(childId) {
            return isSubset.get(childId);
        }, this) >= 0) {
            return false;
        }
        return true;
    }, this);
    util.assert(parents.length <= 1); // there should be only one parent
    var children = atomSets.findAll(function(id, set) {
        return isStrictSuperset.get(id) && !isStrictSuperset.get(this.parent.get(id));
    }, this);
    return {
        'children': children,
        'parent': parents.length === 0 ? -1 : parents[0]
    };
}

chem.SGroupForest.prototype.getPathToRoot = function(sgid) {
    var path = [];
    for (var id = sgid; id >= 0; id = this.parent.get(id)) {
        util.assert(path.indexOf(id) < 0, "SGroupForest: loop detected");
        path.push(id);
    }
    return path;
}

chem.SGroupForest.prototype.validate = function() {
    var atomSets = this.getAtomSets();
    this.molecule.sgroups.each(function(id) {
        this.getPathToRoot(id); // this will throw an exception if there is a loop in the path to root
    }, this);

    var valid = true;
    // 1) child group's atom set is a subset of the parent one's
    this.parent.each(function(id, parentId) {
        if (parentId >= 0 && !util.Set.subset(atomSets.get(id), atomSets.get(parentId)))
            valid = false;
    }, this);

    // 2) siblings have disjoint atom sets
    this.children.each(function(parentId) {
        var list = this.children.get(parentId);
        for (var i = 0; i < list.length; ++i)
            for (var j = i + 1; j < list.length; ++j)
                if (!util.Set.disjoint(atomSets.get(list[i]), atomSets.get(list[j])))
                    valid = false;
    }, this);
    return valid;
}

chem.SGroupForest.prototype.insert = function(id, parent /* int, optional */, children /* [int], optional */) {
    util.assert(!this.parent.has(id), "sgid already present in the forest");
    util.assert(!this.children.has(id), "sgid already present in the forest");

    util.assert(this.validate(), "s-group forest invalid");
    var atomSets = this.getAtomSets();
    var atoms = util.Set.fromList(this.molecule.sgroups.get(id).atoms);
    if (util.isUndefined(parent) || util.isUndefined(children)) { // if these are not provided, deduce automatically
        var guess = this.getAtomSetRelations(id, atoms, atomSets);
        parent = guess.parent;
        children = guess.children;
    }

    // TODO: make children Map<int, Set> instead of Map<int, []>?
    util.each(children, function (childId){ // reset parent links
        util.assert(util.arrayRemoveByValue(this.children.get(this.parent.get(childId)), childId) === 1);
        this.parent.set(childId, id);
    }, this);
    this.children.set(id, children);
    this.parent.set(id, parent);
    this.children.get(parent).push(id);
    util.assert(this.validate(), "s-group forest invalid");
    return {parent: parent, children: children};
}

chem.SGroupForest.prototype.remove = function(id) {
    util.assert(this.parent.has(id), "sgid is not in the forest");
    util.assert(this.children.has(id), "sgid is not in the forest");

    util.assert(this.validate(), "s-group forest invalid");
    var parentId = this.parent.get(id);
    util.each(this.children.get(id), function (childId){ // reset parent links
        this.parent.set(childId, parentId);
        this.children.get(parentId).push(childId);
    }, this);
    util.assert(util.arrayRemoveByValue(this.children.get(parentId), id) === 1);
    this.children.unset(id);
    this.parent.unset(id);
    util.assert(this.validate(), "s-group forest invalid");
}
