/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id$ */

package org.apache.fop.render.pdf;

import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;

import javax.xml.XMLConstants;

import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;

import org.apache.fop.accessibility.StructureTreeElement;
import org.apache.fop.accessibility.StructureTreeEventHandler;
import org.apache.fop.events.EventBroadcaster;
import org.apache.fop.fo.extensions.ExtensionElementMapping;
import org.apache.fop.fo.extensions.InternalElementMapping;
import org.apache.fop.fo.pagination.Flow;
import org.apache.fop.pdf.PDFFactory;
import org.apache.fop.pdf.PDFParentTree;
import org.apache.fop.pdf.PDFStructElem;
import org.apache.fop.pdf.PDFStructTreeRoot;
import org.apache.fop.pdf.StandardStructureAttributes.Table.Scope;
import org.apache.fop.pdf.StandardStructureTypes;
import org.apache.fop.pdf.StandardStructureTypes.Grouping;
import org.apache.fop.pdf.StandardStructureTypes.Table;
import org.apache.fop.pdf.StructureHierarchyMember;
import org.apache.fop.pdf.StructureType;
import org.apache.fop.util.LanguageTags;
import org.apache.fop.util.XMLUtil;

class PDFStructureTreeBuilder implements StructureTreeEventHandler {

    private static final String ROLE = "role";

    private static final Map<String, StructureElementBuilder> BUILDERS
            = new java.util.HashMap<String, StructureElementBuilder>();

    private static final StructureElementBuilder DEFAULT_BUILDER
            = new DefaultStructureElementBuilder(Grouping.NON_STRUCT);

    static {
        // Declarations and Pagination and Layout Formatting Objects
        StructureElementBuilder regionBuilder = new RegionBuilder();
        addBuilder("root",                      StandardStructureTypes.Grouping.DOCUMENT);
        addBuilder("page-sequence",             new PageSequenceBuilder());
        addBuilder("static-content",            regionBuilder);
        addBuilder("flow",                      regionBuilder);
        // Block-level Formatting Objects
        addBuilder("block", new LanguageHolderBuilder(StandardStructureTypes.Paragraphlike.P));
        addBuilder("block-container",           StandardStructureTypes.Grouping.DIV);
        // Inline-level Formatting Objects
        addBuilder("character", new LanguageHolderBuilder(StandardStructureTypes.InlineLevelStructure.SPAN));
        addBuilder("external-graphic",          new ImageBuilder());
        addBuilder("instream-foreign-object",   new ImageBuilder());
        addBuilder("inline",                    new InlineHolderBuilder());
        addBuilder("inline-container",          StandardStructureTypes.Grouping.DIV);
        addBuilder("page-number",               StandardStructureTypes.InlineLevelStructure.QUOTE);
        addBuilder("page-number-citation",      StandardStructureTypes.InlineLevelStructure.QUOTE);
        addBuilder("page-number-citation-last", StandardStructureTypes.InlineLevelStructure.QUOTE);
        // Formatting Objects for Tables
        addBuilder("table-and-caption",         StandardStructureTypes.Grouping.DIV);
        addBuilder("table",                     new TableBuilder());
        addBuilder("table-caption",             StandardStructureTypes.Grouping.CAPTION);
        addBuilder("table-header",              StandardStructureTypes.Table.THEAD);
        addBuilder("table-footer",              new TableFooterBuilder());
        addBuilder("table-body",                StandardStructureTypes.Table.TBODY);
        addBuilder("table-row",                 StandardStructureTypes.Table.TR);
        addBuilder("table-cell",                new TableCellBuilder());
        // Formatting Objects for Lists
        addBuilder("list-block",                StandardStructureTypes.List.L);
        addBuilder("list-item",                 StandardStructureTypes.List.LI);
        addBuilder("list-item-body",            StandardStructureTypes.List.LBODY);
        addBuilder("list-item-label",           StandardStructureTypes.List.LBL);
        // Dynamic Effects: Link and Multi Formatting Objects
        addBuilder("basic-link",                StandardStructureTypes.InlineLevelStructure.LINK);
        // Out-of-Line Formatting Objects
        addBuilder("float",                     StandardStructureTypes.Grouping.DIV);
        addBuilder("footnote",                  StandardStructureTypes.InlineLevelStructure.NOTE);
        addBuilder("footnote-body",             StandardStructureTypes.Grouping.SECT);
        // Other Formatting Objects
        addBuilder("wrapper",                   StandardStructureTypes.InlineLevelStructure.SPAN);
        addBuilder("marker",                    StandardStructureTypes.Grouping.PRIVATE);
        addBuilder("retrieve-marker",           new PlaceholderBuilder());
        addBuilder("retrieve-table-marker",     new PlaceholderBuilder());

        addBuilder("#PCDATA", new PlaceholderBuilder());
    }

    private static void addBuilder(String fo, StructureType structureType) {
        addBuilder(fo, new DefaultStructureElementBuilder(structureType));
    }

    private static void addBuilder(String fo, StructureElementBuilder mapper) {
        BUILDERS.put(fo, mapper);
    }

    private interface StructureElementBuilder {

        PDFStructElem build(StructureHierarchyMember parent, Attributes attributes, PDFFactory pdfFactory,
                EventBroadcaster eventBroadcaster);

    }

    private static class DefaultStructureElementBuilder implements StructureElementBuilder {

        private final StructureType defaultStructureType;

        DefaultStructureElementBuilder(StructureType structureType) {
            this.defaultStructureType = structureType;
        }

        public final PDFStructElem build(StructureHierarchyMember parent, Attributes attributes,
                PDFFactory pdfFactory, EventBroadcaster eventBroadcaster) {
            String role = attributes.getValue(ROLE);
            StructureType structureType;
            if (role == null) {
                structureType = defaultStructureType;
            } else {
                structureType = StandardStructureTypes.get(role);
                if (structureType == null) {
                    structureType = defaultStructureType;
                    PDFEventProducer.Provider.get(eventBroadcaster).nonStandardStructureType(role, role,
                            structureType.toString());
                }
            }
            PDFStructElem structElem = createStructureElement(parent, structureType);
            setAttributes(structElem, attributes);
            addKidToParent(structElem, parent, attributes);
            registerStructureElement(structElem, pdfFactory, attributes);
            return structElem;
        }

        protected PDFStructElem createStructureElement(StructureHierarchyMember parent,
                StructureType structureType) {
            return new PDFStructElem(parent, structureType);
        }

        protected void setAttributes(PDFStructElem structElem, Attributes attributes) {
        }

        protected void addKidToParent(PDFStructElem kid, StructureHierarchyMember parent,
                Attributes attributes) {
            parent.addKid(kid);
        }

        protected void registerStructureElement(PDFStructElem structureElement, PDFFactory pdfFactory,
                Attributes attributes) {
            pdfFactory.getDocument().registerStructureElement(structureElement);
        }

    }

    private static class PageSequenceBuilder extends DefaultStructureElementBuilder {

        PageSequenceBuilder() {
            super(StandardStructureTypes.Grouping.PART);
        }

        @Override
        protected PDFStructElem createStructureElement(StructureHierarchyMember parent,
                StructureType structureType) {
            return new PageSequenceStructElem(parent, structureType);
        }

    }

    private static class RegionBuilder extends DefaultStructureElementBuilder {

        RegionBuilder() {
            super(StandardStructureTypes.Grouping.SECT);
        }

        @Override
        protected void addKidToParent(PDFStructElem kid, StructureHierarchyMember parent,
                Attributes attributes) {
            String flowName = attributes.getValue(Flow.FLOW_NAME);
            ((PageSequenceStructElem) parent).addContent(flowName, kid);
        }

    }

    private static class LanguageHolderBuilder extends DefaultStructureElementBuilder {

        LanguageHolderBuilder(StructureType structureType) {
            super(structureType);
        }

        @Override
        protected void setAttributes(PDFStructElem structElem, Attributes attributes) {
            String xmlLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang");
            if (xmlLang != null) {
                Locale locale = LanguageTags.toLocale(xmlLang);
                structElem.setLanguage(locale);
            }
        }

    }

    private static class InlineHolderBuilder extends DefaultStructureElementBuilder {

        InlineHolderBuilder() {
            super(StandardStructureTypes.InlineLevelStructure.SPAN);
        }

        @Override
        protected void setAttributes(PDFStructElem structElem, Attributes attributes) {
            String text = attributes.getValue(ExtensionElementMapping.URI, "abbreviation");
            if (text != null && !text.equals("")) {
                structElem.put("E", text);
            }
        }
    }

    private static class ImageBuilder extends DefaultStructureElementBuilder {

        ImageBuilder() {
            super(StandardStructureTypes.Illustration.FIGURE);
        }

        @Override
        protected void setAttributes(PDFStructElem structElem, Attributes attributes) {
            String altTextNode = attributes.getValue(ExtensionElementMapping.URI, "alt-text");
            if (altTextNode == null) {
                altTextNode = "No alternate text specified";
            }
            structElem.put("Alt", altTextNode);
        }

    }

    private static class TableBuilder extends DefaultStructureElementBuilder {

        TableBuilder() {
            super(StandardStructureTypes.Table.TABLE);
        }

        @Override
        protected PDFStructElem createStructureElement(StructureHierarchyMember parent,
                StructureType structureType) {
            return new TableStructElem(parent, structureType);
        }
    }

    private static class TableFooterBuilder extends DefaultStructureElementBuilder {

        public TableFooterBuilder() {
            super(StandardStructureTypes.Table.TFOOT);
        }

        @Override
        protected void addKidToParent(PDFStructElem kid, StructureHierarchyMember parent,
                Attributes attributes) {
            ((TableStructElem) parent).addTableFooter(kid);
        }
    }

    private static class TableCellBuilder extends DefaultStructureElementBuilder {

        TableCellBuilder() {
            super(StandardStructureTypes.Table.TD);
        }

        @Override
        protected void registerStructureElement(PDFStructElem structureElement, PDFFactory pdfFactory,
                Attributes attributes) {
            if (structureElement.getStructureType() == Table.TH) {
                String scopeAttribute = attributes.getValue(InternalElementMapping.URI,
                        InternalElementMapping.SCOPE);
                Scope scope = (scopeAttribute == null)
                        ? Scope.COLUMN
                        : Scope.valueOf(scopeAttribute.toUpperCase(Locale.ENGLISH));
                pdfFactory.getDocument().registerStructureElement(structureElement, scope);
            } else {
                pdfFactory.getDocument().registerStructureElement(structureElement);
            }
        }

        @Override
        protected void setAttributes(PDFStructElem structElem, Attributes attributes) {
            String columnSpan = attributes.getValue("number-columns-spanned");
            if (columnSpan != null) {
                structElem.setTableAttributeColSpan(Integer.parseInt(columnSpan));
            }
            String rowSpan = attributes.getValue("number-rows-spanned");
            if (rowSpan != null) {
                structElem.setTableAttributeRowSpan(Integer.parseInt(rowSpan));
            }
        }

    }

    private static class PlaceholderBuilder implements StructureElementBuilder {

        public PDFStructElem build(StructureHierarchyMember parent, Attributes attributes,
                PDFFactory pdfFactory, EventBroadcaster eventBroadcaster) {
            PDFStructElem elem = new PDFStructElem.Placeholder(parent);
            parent.addKid(elem);
            return elem;
        }

    }

    private PDFFactory pdfFactory;

    private EventBroadcaster eventBroadcaster;

    private LinkedList<PDFStructElem> ancestors = new LinkedList<PDFStructElem>();

    private PDFStructElem rootStructureElement;

    void setPdfFactory(PDFFactory pdfFactory) {
        this.pdfFactory = pdfFactory;
    }

    void setEventBroadcaster(EventBroadcaster eventBroadcaster) {
        this.eventBroadcaster = eventBroadcaster;
    }

    void setLogicalStructureHandler(PDFLogicalStructureHandler logicalStructureHandler) {
        createRootStructureElement(logicalStructureHandler);
    }

    private void createRootStructureElement(PDFLogicalStructureHandler logicalStructureHandler) {
        assert rootStructureElement == null;
        PDFParentTree parentTree = logicalStructureHandler.getParentTree();
        PDFStructTreeRoot structTreeRoot = pdfFactory.getDocument().makeStructTreeRoot(parentTree);
        rootStructureElement = createStructureElement("root", structTreeRoot,
                new AttributesImpl(), pdfFactory, eventBroadcaster);
    }

    private static PDFStructElem createStructureElement(String name, StructureHierarchyMember parent,
                Attributes attributes, PDFFactory pdfFactory, EventBroadcaster eventBroadcaster) {
            StructureElementBuilder builder = BUILDERS.get(name);
            if (builder == null) {
                // TODO is a fallback really necessary?
                builder = DEFAULT_BUILDER;
            }
            return builder.build(parent, attributes, pdfFactory, eventBroadcaster);
        }

    public void startPageSequence(Locale language, String role) {
        ancestors = new LinkedList<PDFStructElem>();
        AttributesImpl attributes = new AttributesImpl();
        attributes.addAttribute("", ROLE, ROLE, XMLUtil.CDATA, role);
        PDFStructElem structElem = createStructureElement("page-sequence",
                rootStructureElement, attributes, pdfFactory, eventBroadcaster);
        if (language != null) {
            structElem.setLanguage(language);
        }
        ancestors.add(structElem);
    }

    public void endPageSequence() {
    }

    public StructureTreeElement startNode(String name, Attributes attributes, StructureTreeElement parent) {
        if (!isPDFA1Safe(name)) {
            return null;
        }
        assert parent == null || parent instanceof PDFStructElem;
        PDFStructElem parentElem = parent == null ? ancestors.getFirst() : (PDFStructElem) parent;
        PDFStructElem structElem = createStructureElement(name, parentElem, attributes,
                pdfFactory, eventBroadcaster);
        ancestors.addFirst(structElem);
        return structElem;
    }

    public void endNode(String name) {
        if (isPDFA1Safe(name)) {
            ancestors.removeFirst();
        }
    }

    private boolean isPDFA1Safe(String name) {
        return !(pdfFactory.getDocument().getProfile().getPDFAMode().isPart1()
                && (name.equals("table-body")
                || name.equals("table-header")
                || name.equals("table-footer")));
    }

    public StructureTreeElement startImageNode(String name, Attributes attributes, StructureTreeElement parent) {
        return startNode(name, attributes, parent);
    }

    public StructureTreeElement startReferencedNode(String name, Attributes attributes, StructureTreeElement parent) {
        return startNode(name, attributes, parent);
    }

}
