/*
 * Routines for resolving ID clashes when importing or pasting.
 *
 * Authors:
 *   Stephen Silver <sasilver@users.sourceforge.net>
 *   Jon A. Cruz <jon@joncruz.org>
 *   Abhishek Sharma
 *
 * Copyright (C) 2008 authors
 *
 * Released under GNU GPL, read the file 'COPYING' for more information
 */

#include <cstdlib>
#include <cstring>
#include <list>
#include <map>
#include <string>
#include <utility>

#include "extract-uri.h"
#include "id-clash.h"
#include "sp-object.h"
#include "style.h"
#include "sp-paint-server.h"
#include "xml/node.h"
#include "xml/repr.h"
#include "sp-root.h"
#include "sp-gradient.h"

typedef enum { REF_HREF, REF_STYLE, REF_URL, REF_CLIPBOARD } ID_REF_TYPE;

struct IdReference {
    ID_REF_TYPE type;
    SPObject *elem;
    const char *attr;  // property or href-like attribute
};

typedef std::map<Glib::ustring, std::list<IdReference> > refmap_type;

typedef std::pair<SPObject*, Glib::ustring> id_changeitem_type;
typedef std::list<id_changeitem_type> id_changelist_type;

const char *href_like_attributes[] = {
    "inkscape:connection-end",
    "inkscape:connection-start",
    "inkscape:href",
    "inkscape:path-effect",
    "inkscape:perspectiveID",
    "inkscape:tiled-clone-of",
    "xlink:href",
};
#define NUM_HREF_LIKE_ATTRIBUTES (sizeof(href_like_attributes) / sizeof(*href_like_attributes))

const SPIPaint SPStyle::* SPIPaint_members[] = {
    //&SPStyle::color,
    &SPStyle::fill,
    &SPStyle::stroke,
};
const char* SPIPaint_properties[] = {
    //"color",
    "fill",
    "stroke",
};
#define NUM_SPIPAINT_PROPERTIES (sizeof(SPIPaint_properties) / sizeof(*SPIPaint_properties))

const char* other_url_properties[] = {
    "clip-path",
    "color-profile",
    "cursor",
    "marker-end",
    "marker-mid",
    "marker-start",
    "mask",
};
#define NUM_OTHER_URL_PROPERTIES (sizeof(other_url_properties) / sizeof(*other_url_properties))

const char* clipboard_properties[] = {
    //"color",
    "fill",
    "filter",
    "stroke",
    "marker-end",
    "marker-mid",
    "marker-start"
};
#define NUM_CLIPBOARD_PROPERTIES (sizeof(clipboard_properties) / sizeof(*clipboard_properties))

/**
 *  Build a table of places where IDs are referenced, for a given element.
 *  FIXME: There are some types of references not yet dealt with here
 *         (e.g., ID selectors in CSS stylesheets, and references in scripts).
 */
static void
find_references(SPObject *elem, refmap_type &refmap)
{
    if (elem->cloned) return;
    Inkscape::XML::Node *repr_elem = elem->getRepr();
    if (!repr_elem) return;
    if (repr_elem->type() != Inkscape::XML::ELEMENT_NODE) return;

    /* check for references in inkscape:clipboard elements */
    if (!std::strcmp(repr_elem->name(), "inkscape:clipboard")) {
        SPCSSAttr *css = sp_repr_css_attr(repr_elem, "style");
        if (css) {
            for (unsigned i = 0; i < NUM_CLIPBOARD_PROPERTIES; ++i) {
                const char *attr = clipboard_properties[i];
                const gchar *value = sp_repr_css_property(css, attr, NULL);
                if (value) {
                    gchar *uri = extract_uri(value);
                    if (uri && uri[0] == '#') {
                        IdReference idref = { REF_CLIPBOARD, elem, attr };
                        refmap[uri+1].push_back(idref);
                    }
                    g_free(uri);
                }
            }

        }
        return; // nothing more to do for inkscape:clipboard elements
    }

    /* check for xlink:href="#..." and similar */
    for (unsigned i = 0; i < NUM_HREF_LIKE_ATTRIBUTES; ++i) {
        const char *attr = href_like_attributes[i];
        const gchar *val = repr_elem->attribute(attr);
        if (val && val[0] == '#') {
            std::string id(val+1);
            IdReference idref = { REF_HREF, elem, attr };
            refmap[id].push_back(idref);
        }
    }

    SPStyle *style = elem->style;

    /* check for url(#...) references in 'fill' or 'stroke' */
    for (unsigned i = 0; i < NUM_SPIPAINT_PROPERTIES; ++i) {
        const SPIPaint SPStyle::*prop = SPIPaint_members[i];
        const SPIPaint *paint = &(style->*prop);
        if (paint->isPaintserver() && paint->value.href) {
            const SPObject *obj = paint->value.href->getObject();
            if (obj) {
                const gchar *id = obj->getId();
                IdReference idref = { REF_STYLE, elem, SPIPaint_properties[i] };
                refmap[id].push_back(idref);
            }
        }
    }

    /* check for url(#...) references in 'filter' */
    const SPIFilter *filter = &(style->filter);
    if (filter->href) {
        const SPObject *obj = filter->href->getObject();
        if (obj) {
            const gchar *id = obj->getId();
            IdReference idref = { REF_STYLE, elem, "filter" };
            refmap[id].push_back(idref);
        }
    }

    /* check for url(#...) references in markers */
    const gchar *markers[4] = { "", "marker-start", "marker-mid", "marker-end" };
    for (unsigned i = SP_MARKER_LOC_START; i < SP_MARKER_LOC_QTY; i++) {
        const gchar *value = style->marker_ptrs[i]->value;
        if (value) {
            gchar *uri = extract_uri(value);
            if (uri && uri[0] == '#') {
                IdReference idref = { REF_STYLE, elem, markers[i] };
                refmap[uri+1].push_back(idref);
            }
            g_free(uri);
        }
    }

    /* check for other url(#...) references */
    for (unsigned i = 0; i < NUM_OTHER_URL_PROPERTIES; ++i) {
        const char *attr = other_url_properties[i];
        const gchar *value = repr_elem->attribute(attr);
        if (value) {
            gchar *uri = extract_uri(value);
            if (uri && uri[0] == '#') {
                IdReference idref = { REF_URL, elem, attr };
                refmap[uri+1].push_back(idref);
            }
            g_free(uri);
        }
    }
    
    // recurse
    for (SPObject *child = elem->firstChild(); child; child = child->getNext() )
    {
        find_references(child, refmap);
    }
}

/**
 *  Change any IDs that clash with IDs in the current document, and make
 *  a list of those changes that will require fixing up references.
 */
static void
change_clashing_ids(SPDocument *imported_doc, SPDocument *current_doc,
                    SPObject *elem, refmap_type const &refmap,
                    id_changelist_type *id_changes)
{
    const gchar *id = elem->getId();
    bool fix_clashing_ids = true;

    if (id && current_doc->getObjectById(id)) {
        // Choose a new ID.
        // To try to preserve any meaningfulness that the original ID
        // may have had, the new ID is the old ID followed by a hyphen
        // and one or more digits.
        
        if (SP_IS_GRADIENT(elem)) {
            SPObject *cd_obj =  current_doc->getObjectById(id);

            if (cd_obj && SP_IS_GRADIENT(cd_obj)) {
                SPGradient *cd_gr = SP_GRADIENT(cd_obj);
                if ( cd_gr->isEquivalent(SP_GRADIENT(elem))) {
                    fix_clashing_ids = false;
                 }
             }
        }
        
        if (fix_clashing_ids) {
            std::string old_id(id);
            std::string new_id(old_id + '-');
            for (;;) {
                new_id += "0123456789"[std::rand() % 10];
                const char *str = new_id.c_str();
                if (current_doc->getObjectById(str) == NULL &&
                    imported_doc->getObjectById(str) == NULL) break;
            }
            // Change to the new ID

            elem->getRepr()->setAttribute("id", new_id);
                // Make a note of this change, if we need to fix up refs to it
            if (refmap.find(old_id) != refmap.end())
            id_changes->push_back(id_changeitem_type(elem, old_id));
        }
    }


    // recurse
    for (SPObject *child = elem->firstChild(); child; child = child->getNext() )
    {
        change_clashing_ids(imported_doc, current_doc, child, refmap, id_changes);
    }
}

/**
 *  Fix up references to changed IDs.
 */
static void
fix_up_refs(refmap_type const &refmap, const id_changelist_type &id_changes)
{
    id_changelist_type::const_iterator pp;
    const id_changelist_type::const_iterator pp_end = id_changes.end();
    for (pp = id_changes.begin(); pp != pp_end; ++pp) {
        SPObject *obj = pp->first;
        refmap_type::const_iterator pos = refmap.find(pp->second);
        std::list<IdReference>::const_iterator it;
        const std::list<IdReference>::const_iterator it_end = pos->second.end();
        for (it = pos->second.begin(); it != it_end; ++it) {
            switch (it->type) {
                case REF_HREF: {
                    gchar *new_uri = g_strdup_printf("#%s", obj->getId());
                    it->elem->getRepr()->setAttribute(it->attr, new_uri);
                    g_free(new_uri);
                    break;
                }
                case REF_STYLE: {
                    sp_style_set_property_url(it->elem, it->attr, obj, false);
                    break;
                }
                case REF_URL: {
                    gchar *url = g_strdup_printf("url(#%s)", obj->getId());
                    it->elem->getRepr()->setAttribute(it->attr, url);
                    g_free(url);
                    break;
                }
                case REF_CLIPBOARD: {
                    SPCSSAttr *style = sp_repr_css_attr(it->elem->getRepr(), "style");
                    gchar *url = g_strdup_printf("url(#%s)", obj->getId());
                    sp_repr_css_set_property(style, it->attr, url);
                    g_free(url);
                    Glib::ustring style_string;
                    sp_repr_css_write_string(style, style_string);
                    it->elem->getRepr()->setAttribute("style", style_string);
                    break;
                }
            }
        }
    }
}

/**
 *  This function resolves ID clashes between the document being imported
 *  and the current open document: IDs in the imported document that would
 *  clash with IDs in the existing document are changed, and references to
 *  those IDs are updated accordingly.
 */
void
prevent_id_clashes(SPDocument *imported_doc, SPDocument *current_doc)
{
    refmap_type refmap;
    id_changelist_type id_changes;
    SPObject *imported_root = imported_doc->getRoot();
        
    find_references(imported_root, refmap);
    change_clashing_ids(imported_doc, current_doc, imported_root, refmap,
                        &id_changes);
    fix_up_refs(refmap, id_changes);
}

/*
 * Change any references of svg:def from_obj into to_obj
 */
void
change_def_references(SPObject *from_obj, SPObject *to_obj)
{
    refmap_type refmap;
    SPDocument *current_doc = from_obj->document;
    std::string old_id(from_obj->getId());

    find_references(current_doc->getRoot(), refmap);

    refmap_type::const_iterator pos = refmap.find(old_id);
    if (pos != refmap.end()) {
        std::list<IdReference>::const_iterator it;
        const std::list<IdReference>::const_iterator it_end = pos->second.end();
        for (it = pos->second.begin(); it != it_end; ++it) {
            switch (it->type) {
                case REF_HREF: {
                    gchar *new_uri = g_strdup_printf("#%s", to_obj->getId());
                    it->elem->getRepr()->setAttribute(it->attr, new_uri);
                    g_free(new_uri);
                    break;
                }
                case REF_STYLE: {
                    sp_style_set_property_url(it->elem, it->attr, to_obj, false);
                    break;
                }
                case REF_URL: {
                    gchar *url = g_strdup_printf("url(#%s)", to_obj->getId());
                    it->elem->getRepr()->setAttribute(it->attr, url);
                    g_free(url);
                    break;
                }
                case REF_CLIPBOARD: {
                    SPCSSAttr *style = sp_repr_css_attr(it->elem->getRepr(), "style");
                    gchar *url = g_strdup_printf("url(#%s)", to_obj->getId());
                    sp_repr_css_set_property(style, it->attr, url);
                    g_free(url);
                    Glib::ustring style_string;
                    sp_repr_css_write_string(style, style_string);
                    it->elem->getRepr()->setAttribute("style", style_string);
                    break;
                }
            } 
        }
    }
}

/*
 * Change the id of a SPObject to new_name
 * If there is an id clash then rename to something similar
 */
void rename_id(SPObject *elem, Glib::ustring const &new_name)
{
    if (new_name.empty()){
        g_message("Invalid Id, will not change.");
        return;
    }
    gchar *id = g_strdup(new_name.c_str()); //id is not empty here as new_name is check to be not empty
    g_strcanon (id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_');
    Glib::ustring new_name2 = id; //will not fail as id can not be NULL, see length check on new_name
    if (!isalnum (new_name2[0])) {
        g_message("Invalid Id, will not change.");
        g_free (id);
        return;
    }

    SPDocument *current_doc = elem->document;
    refmap_type refmap;
    find_references(current_doc->getRoot(), refmap);

    std::string old_id(elem->getId());
    if (current_doc->getObjectById(id)) {
        // Choose a new ID.
        // To try to preserve any meaningfulness that the original ID
        // may have had, the new ID is the old ID followed by a hyphen
        // and one or more digits.
        new_name2 += '-';
        for (;;) {
            new_name2 += "0123456789"[std::rand() % 10];
            if (current_doc->getObjectById(new_name2) == NULL)
                break;
        }
    }
    g_free (id);
    // Change to the new ID
    elem->getRepr()->setAttribute("id", new_name2);
    // Make a note of this change, if we need to fix up refs to it
    id_changelist_type id_changes;
    if (refmap.find(old_id) != refmap.end()) {
        id_changes.push_back(id_changeitem_type(elem, old_id));
    }

    fix_up_refs(refmap, id_changes);
}

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
