/*
 * 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.accessibility.fo;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;

import javax.xml.XMLConstants;

import org.xml.sax.helpers.AttributesImpl;

import org.apache.fop.accessibility.StructureTreeElement;
import org.apache.fop.accessibility.StructureTreeEventHandler;
import org.apache.fop.fo.FOEventHandler;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FOText;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.extensions.ExtensionElementMapping;
import org.apache.fop.fo.extensions.InternalElementMapping;
import org.apache.fop.fo.flow.AbstractRetrieveMarker;
import org.apache.fop.fo.flow.BasicLink;
import org.apache.fop.fo.flow.Block;
import org.apache.fop.fo.flow.BlockContainer;
import org.apache.fop.fo.flow.Character;
import org.apache.fop.fo.flow.ExternalGraphic;
import org.apache.fop.fo.flow.Footnote;
import org.apache.fop.fo.flow.FootnoteBody;
import org.apache.fop.fo.flow.Inline;
import org.apache.fop.fo.flow.InstreamForeignObject;
import org.apache.fop.fo.flow.ListBlock;
import org.apache.fop.fo.flow.ListItem;
import org.apache.fop.fo.flow.ListItemBody;
import org.apache.fop.fo.flow.ListItemLabel;
import org.apache.fop.fo.flow.PageNumber;
import org.apache.fop.fo.flow.PageNumberCitation;
import org.apache.fop.fo.flow.PageNumberCitationLast;
import org.apache.fop.fo.flow.RetrieveMarker;
import org.apache.fop.fo.flow.RetrieveTableMarker;
import org.apache.fop.fo.flow.Wrapper;
import org.apache.fop.fo.flow.table.Table;
import org.apache.fop.fo.flow.table.TableBody;
import org.apache.fop.fo.flow.table.TableCell;
import org.apache.fop.fo.flow.table.TableFooter;
import org.apache.fop.fo.flow.table.TableHeader;
import org.apache.fop.fo.flow.table.TableRow;
import org.apache.fop.fo.pagination.Flow;
import org.apache.fop.fo.pagination.LayoutMasterSet;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.pagination.Root;
import org.apache.fop.fo.pagination.StaticContent;
import org.apache.fop.fo.properties.CommonAccessibilityHolder;
import org.apache.fop.fo.properties.CommonHyphenation;
import org.apache.fop.util.LanguageTags;
import org.apache.fop.util.XMLUtil;

/**
 * A bridge between {@link FOEventHandler} and {@link StructureTreeEventHandler}.
 */
class StructureTreeEventTrigger extends FOEventHandler {

    private StructureTreeEventHandler structureTreeEventHandler;

    private LayoutMasterSet layoutMasterSet;

    private Stack<Table> tables = new Stack<Table>();

    private Stack<Boolean> inTableHeader = new Stack<Boolean>();

    private Stack<Locale> locales = new Stack<Locale>();

    private final Map<AbstractRetrieveMarker, State> states = new HashMap<AbstractRetrieveMarker, State>();

    private static final class State {

        private final Stack<Table> tables;

        private final Stack<Boolean> inTableHeader;

        private final Stack<Locale> locales;

        @SuppressWarnings("unchecked")
        State(StructureTreeEventTrigger o) {
            this.tables = (Stack<Table>) o.tables.clone();
            this.inTableHeader = (Stack<Boolean>) o.inTableHeader.clone();
            this.locales = (Stack<Locale>) o.locales.clone();
        }

    }

    public StructureTreeEventTrigger(StructureTreeEventHandler structureTreeEventHandler) {
        this.structureTreeEventHandler = structureTreeEventHandler;
    }

    @Override
    public void startRoot(Root root) {
        locales.push(root.getLocale());
    }

    @Override
    public void endRoot(Root root) {
        locales.pop();
    }

    @Override
    public void startPageSequence(PageSequence pageSeq) {
        if (layoutMasterSet == null) {
            layoutMasterSet = pageSeq.getRoot().getLayoutMasterSet();
        }
        Locale locale = pageSeq.getLocale();
        if (locale != null) {
            locales.push(locale);
        } else {
            locales.push(locales.peek());
        }
        String role = pageSeq.getCommonAccessibility().getRole();
        structureTreeEventHandler.startPageSequence(locale, role);
    }

    @Override
    public void endPageSequence(PageSequence pageSeq) {
        structureTreeEventHandler.endPageSequence();
        locales.pop();
    }

    @Override
    public void startPageNumber(PageNumber pagenum) {
        startElementWithID(pagenum);
    }

    @Override
    public void endPageNumber(PageNumber pagenum) {
        endElement(pagenum);
    }

    @Override
    public void startPageNumberCitation(PageNumberCitation pageCite) {
        startElementWithID(pageCite);
    }

    @Override
    public void endPageNumberCitation(PageNumberCitation pageCite) {
        endElement(pageCite);
    }

    @Override
    public void startPageNumberCitationLast(PageNumberCitationLast pageLast) {
        startElementWithID(pageLast);
    }

    @Override
    public void endPageNumberCitationLast(PageNumberCitationLast pageLast) {
        endElement(pageLast);
    }

    @Override
    public void startStatic(StaticContent staticContent) {
        AttributesImpl flowName = createFlowNameAttribute(staticContent.getFlowName());
        startElement(staticContent, flowName);
    }

    private AttributesImpl createFlowNameAttribute(String flowName) {
        String regionName = layoutMasterSet.getDefaultRegionNameFor(flowName);
        AttributesImpl attribute = new AttributesImpl();
        addNoNamespaceAttribute(attribute, Flow.FLOW_NAME, regionName);
        return attribute;
    }

    @Override
    public void endStatic(StaticContent staticContent) {
        endElement(staticContent);
    }

    @Override
    public void startFlow(Flow fl) {
        AttributesImpl flowName = createFlowNameAttribute(fl.getFlowName());
        startElement(fl, flowName);
    }

    @Override
    public void endFlow(Flow fl) {
        endElement(fl);
    }

    @Override
    public void startBlock(Block bl) {
        CommonHyphenation hyphProperties = bl.getCommonHyphenation();
        AttributesImpl attributes = createLangAttribute(hyphProperties);
        startElement(bl, attributes);
    }

    private AttributesImpl createLangAttribute(CommonHyphenation hyphProperties) {
        Locale locale = hyphProperties.getLocale();
        AttributesImpl attributes = new AttributesImpl();
        if (locale == null || locale.equals(locales.peek())) {
            locales.push(locales.peek());
        } else {
            locales.push(locale);
            addAttribute(attributes, XMLConstants.XML_NS_URI, "lang", "xml",
                    LanguageTags.toLanguageTag(locale));
        }
        return attributes;
    }

    @Override
    public void endBlock(Block bl) {
        endElement(bl);
        locales.pop();
    }

    @Override
    public void startBlockContainer(BlockContainer blc) {
        startElement(blc);
    }

    @Override
    public void endBlockContainer(BlockContainer blc) {
        endElement(blc);
    }

    @Override
    public void startInline(Inline inl) {
        startElement(inl);
    }

    @Override
    public void endInline(Inline inl) {
        endElement(inl);
    }

    @Override
    public void startTable(Table tbl) {
        tables.push(tbl);
        startElement(tbl);
    }

    @Override
    public void endTable(Table tbl) {
        endElement(tbl);
        tables.pop();
    }

    @Override
    public void startHeader(TableHeader header) {
        inTableHeader.push(Boolean.TRUE);
        startElement(header);
    }

    @Override
    public void endHeader(TableHeader header) {
        endElement(header);
        inTableHeader.pop();
    }

    @Override
    public void startFooter(TableFooter footer) {
        // TODO Shouldn't it be true?
        inTableHeader.push(Boolean.FALSE);
        startElement(footer);
    }

    @Override
    public void endFooter(TableFooter footer) {
        endElement(footer);
        inTableHeader.pop();
    }

    @Override
    public void startBody(TableBody body) {
        inTableHeader.push(Boolean.FALSE);
        startElement(body);
    }

    @Override
    public void endBody(TableBody body) {
        endElement(body);
        inTableHeader.pop();
    }

    @Override
    public void startRow(TableRow tr) {
        startElement(tr);
    }

    @Override
    public void endRow(TableRow tr) {
        endElement(tr);
    }

    @Override
    public void startCell(TableCell tc) {
        AttributesImpl attributes = new AttributesImpl();
        addSpanAttribute(attributes, "number-columns-spanned", tc.getNumberColumnsSpanned());
        addSpanAttribute(attributes, "number-rows-spanned", tc.getNumberRowsSpanned());
        boolean rowHeader = inTableHeader.peek();
        boolean columnHeader = tables.peek().getColumn(tc.getColumnNumber() - 1).isHeader();
        if (rowHeader || columnHeader) {
            final String th = "TH";
            String role = tc.getCommonAccessibility().getRole();
            /* Do not override a custom role */
            if (role == null) {
                role = th;
                addNoNamespaceAttribute(attributes, "role", th);
            }
            if (role.equals(th)) {
                if (columnHeader) {
                    String scope = rowHeader ? "Both" : "Row";
                    addAttribute(attributes, InternalElementMapping.URI, InternalElementMapping.SCOPE,
                            InternalElementMapping.STANDARD_PREFIX, scope);
                }
            }
        }
        startElement(tc, attributes);
    }

    private void addSpanAttribute(AttributesImpl attributes, String attributeName, int span) {
        if (span > 1) {
            addNoNamespaceAttribute(attributes, attributeName, Integer.toString(span));
        }
    }

    @Override
    public void endCell(TableCell tc) {
        endElement(tc);
    }

    @Override
    public void startList(ListBlock lb) {
        startElement(lb);
    }

    @Override
    public void endList(ListBlock lb) {
        endElement(lb);
    }

    @Override
    public void startListItem(ListItem li) {
        startElement(li);
    }

    @Override
    public void endListItem(ListItem li) {
        endElement(li);
    }

    @Override
    public void startListLabel(ListItemLabel listItemLabel) {
        startElement(listItemLabel);
    }

    @Override
    public void endListLabel(ListItemLabel listItemLabel) {
        endElement(listItemLabel);
    }

    @Override
    public void startListBody(ListItemBody listItemBody) {
        startElement(listItemBody);
    }

    @Override
    public void endListBody(ListItemBody listItemBody) {
        endElement(listItemBody);
    }

    @Override
    public void startLink(BasicLink basicLink) {
        startElementWithIDAndAltText(basicLink, basicLink.getAltText());
    }

    @Override
    public void endLink(BasicLink basicLink) {
        endElement(basicLink);
    }

    @Override
    public void image(ExternalGraphic eg) {
        startElementWithIDAndAltText(eg, eg.getAltText());
        endElement(eg);
    }

    @Override
    public void startInstreamForeignObject(InstreamForeignObject ifo) {
        startElementWithIDAndAltText(ifo, ifo.getAltText());
    }

    @Override
    public void endInstreamForeignObject(InstreamForeignObject ifo) {
        endElement(ifo);
    }

    @Override
    public void startFootnote(Footnote footnote) {
        startElement(footnote);
    }

    @Override
    public void endFootnote(Footnote footnote) {
        endElement(footnote);
    }

    @Override
    public void startFootnoteBody(FootnoteBody body) {
        startElement(body);
    }

    @Override
    public void endFootnoteBody(FootnoteBody body) {
        endElement(body);
    }

    @Override
    public void startWrapper(Wrapper wrapper) {
        startElement(wrapper);
    }

    @Override
    public void endWrapper(Wrapper wrapper) {
        endElement(wrapper);
    }

    @Override
    public void startRetrieveMarker(RetrieveMarker retrieveMarker) {
        startElementWithID(retrieveMarker);
        saveState(retrieveMarker);
    }

    void saveState(AbstractRetrieveMarker retrieveMarker) {
        states.put(retrieveMarker, new State(this));
    }

    @Override
    public void endRetrieveMarker(RetrieveMarker retrieveMarker) {
        endElement(retrieveMarker);
    }

    @Override
    public void restoreState(RetrieveMarker retrieveMarker) {
        restoreRetrieveMarkerState(retrieveMarker);
    }

    @SuppressWarnings("unchecked")
    private void restoreRetrieveMarkerState(AbstractRetrieveMarker retrieveMarker) {
        State state = states.get(retrieveMarker);
        tables = (Stack<Table>) state.tables.clone();
        inTableHeader = (Stack<Boolean>) state.inTableHeader.clone();
        locales = (Stack<Locale>) state.locales.clone();
    }

    @Override
    public void startRetrieveTableMarker(RetrieveTableMarker retrieveTableMarker) {
        startElementWithID(retrieveTableMarker);
        saveState(retrieveTableMarker);
    }

    @Override
    public void endRetrieveTableMarker(RetrieveTableMarker retrieveTableMarker) {
        endElement(retrieveTableMarker);
    }

    @Override
    public void restoreState(RetrieveTableMarker retrieveTableMarker) {
        restoreRetrieveMarkerState(retrieveTableMarker);
    }

    @Override
    public void character(Character c) {
        AttributesImpl attributes = createLangAttribute(c.getCommonHyphenation());
        startElementWithID(c, attributes);
        endElement(c);
        locales.pop();
    }

    @Override
    public void characters(FOText foText) {
        startElementWithID(foText);
        endElement(foText);
    }


    private StructureTreeElement startElement(FONode node) {
        AttributesImpl attributes = new AttributesImpl();
        if (node instanceof Inline) {
            Inline in = (Inline)node;
            if (!in.getAbbreviation().equals("")) {
                addAttribute(attributes, ExtensionElementMapping.URI, "abbreviation",
                        ExtensionElementMapping.STANDARD_PREFIX, in.getAbbreviation());
            }
        }
        return startElement(node, attributes);
    }

    private void startElementWithID(FONode node) {
        startElementWithID(node, new AttributesImpl());
    }

    private void startElementWithID(FONode node, AttributesImpl attributes) {
        String localName = node.getLocalName();
        if (node instanceof CommonAccessibilityHolder) {
            addRole((CommonAccessibilityHolder) node, attributes);
        }
        node.setStructureTreeElement(
                structureTreeEventHandler.startReferencedNode(localName, attributes,
                        node.getParent().getStructureTreeElement()));
    }

    private void startElementWithIDAndAltText(FObj node, String altText) {
        AttributesImpl attributes = new AttributesImpl();
        String localName = node.getLocalName();
        addRole((CommonAccessibilityHolder)node, attributes);
        addAttribute(attributes, ExtensionElementMapping.URI, "alt-text",
                ExtensionElementMapping.STANDARD_PREFIX, altText);
        node.setStructureTreeElement(
                structureTreeEventHandler.startImageNode(localName, attributes,
                        node.getParent().getStructureTreeElement()));
    }

    private StructureTreeElement startElement(FONode node, AttributesImpl attributes) {
        String localName = node.getLocalName();
        if (node instanceof CommonAccessibilityHolder) {
            addRole((CommonAccessibilityHolder) node, attributes);
        }
        return structureTreeEventHandler.startNode(localName, attributes,
                node.getParent().getStructureTreeElement());
    }

    private void addNoNamespaceAttribute(AttributesImpl attributes, String name, String value) {
        attributes.addAttribute("", name, name, XMLUtil.CDATA, value);
    }

    private void addAttribute(AttributesImpl attributes,
            String namespace, String localName, String prefix, String value) {
        assert namespace.length() > 0 && prefix.length() > 0;
        String qualifiedName = prefix + ":" + localName;
        attributes.addAttribute(namespace, localName, qualifiedName, XMLUtil.CDATA, value);
    }

    private void addRole(CommonAccessibilityHolder node, AttributesImpl attributes) {
        String role = node.getCommonAccessibility().getRole();
        if (role != null) {
            addNoNamespaceAttribute(attributes, "role", role);
        }
    }

    private void endElement(FONode node) {
        String localName = node.getLocalName();
        structureTreeEventHandler.endNode(localName);
    }

}
