package votorola.s.gwt.mediawiki; // Copyright 2012-2013, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.

import com.google.gwt.core.client.*;
import com.google.gwt.dom.client.*;
import com.google.gwt.http.client.URL;
import com.google.gwt.jsonp.client.JsonpRequestBuilder;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.google.web.bindery.event.shared.HandlerRegistration;
import votorola.a.count.gwt.*;
import votorola.a.diff.*;
import votorola.a.web.gwt.*;
import votorola.g.lang.*;
import votorola.g.web.gwt.*;
import votorola.g.web.gwt.event.*;
import votorola.s.gwt.stage.*;
import votorola.s.gwt.stage.vote.*;

import static votorola.a.count.CountNode.DART_SECTOR_MAX;
import static votorola.s.gwt.stage.vote.LightableDifference.REL_CANDIDATE;
import static votorola.s.gwt.stage.vote.LightableDifference.REL_CO;
import static votorola.s.gwt.stage.vote.LightableDifference.REL_TIGHT_CYCLE;
import static votorola.s.gwt.stage.vote.LightableDifference.REL_VOTER;


/** A factory of {@linkplain DifferenceShadows difference shadows} for MediaWiki.
  */
final class DifferenceShadower implements AsyncCallback<JavaScriptObject>, CountCacheListener
{


    /** Creates a DifferenceShadower that enables difference shadowing if the page is a
      * position draft in correct form, or otherwise does nothing.  If shadows are
      * enabled, then stage property {@linkplain Stage#areDifferencesShadowed()
      * differencesShadowed} is set true.
      */
    DifferenceShadower()
    {
        Stage.i().addInitializer( new TheatreInitializer() // auto-removed
        {
            public void initFrom( Stage _s, boolean _rPending ) {}
            public void initFromComplete( final Stage s, boolean _rPending ) { stageReady( s ); }
            public void initTo( Stage _s ) {} // rPending
            public void initTo( final Stage s, final TheatrePage r ) { initR( s, r ); }
            public void initToComplete( final Stage s, boolean _rPending ) { stageReady( s ); }
            public void initUltimately( final Stage s, final TheatrePage r ) { initR( s, r ); }

            private void initR( final Stage s, final TheatrePage r )
              // called earlier or later than stageReady(s), if at all
            {
                if( shadows == null ) referrer = r; // set later
                else shadows.setReferrer( r );      // or set now
            }
        });
        // eventually continues at stageReady
    }



   // - A s y n c - C a l l b a c k ------------------------------------------------------


    public void onFailure( final Throwable x )
    {
        Stage.i().addWarning( App.i().mesS().gwt_mediawiki_DifferenceShadower_requestFail( x,
          requestLoc ));
    }



    public void onSuccess( final JavaScriptObject response )
    {
        final Stage stage = Stage.i();
        final JavaScriptObject d = response._get( "d" );
        final JsArrayString errors = d._get( "error" );
        if( errors != null ) for( int e = 0, eN = errors.length(); e < eN; ++e )
        {
            stage.addWarning( App.i().mesS().gwt_mediawiki_DifferenceShadower_requestWarn(
              errors.get(e), requestLoc ));
        }
        ResponseABReader res = new ResponseABReader( d, "A" ); // voters and/or peers
        final ShadowedDiff[] voterDiffBoard = new ShadowedDiff[DART_SECTOR_MAX];
        final ShadowedDiff[] peerDiffBoard = new ShadowedDiff[DART_SECTOR_MAX];
        int n = 0;
        if( res.hasNext() )
        {
            res.next();
            n = onSuccess_init( anchorNode.voters(), 0, res, voterDiffBoard, REL_VOTER );
            if( res.isEnd() ) n = 0;
            else // remainder must be peers
            {
                assert n == DART_SECTOR_MAX;
                n = onSuccess_init( peers, 0, res, peerDiffBoard, REL_CO );
                assert !res.hasNext();
            }
        }
        res = new ResponseABReader( d, "B" ); // peers and/or candidate
        ShadowedDiff candidateDiff = null; // unless found
        if( res.hasNext() )
        {
            res.next();
            n = onSuccess_init( peers, n, res, peerDiffBoard, REL_CO );
            candidate: if( !res.isEnd() ) // remainder must be candidate
            {
                assert n == DART_SECTOR_MAX;
                if( candidateNode != null )
                {
                    candidateDiff = res.diffOrNull;
                    if( candidateDiff == null ) break candidate; // diff unknown

                    final String candidateDiffKey = res.keyOrNull; assert candidateDiffKey != null;
                    res.verifyMatch( candidateNode.name() );
                    assert !res.hasNext();
                    final char rel;
                    if( tightCyclerPosition == 'b' ) // anchor (a) candidate (b)
                    {
                        n = candidateNode.dartSector() - 1;
                        assert voterDiffBoard[n] == null;
                        voterDiffBoard[n] = candidateDiff; // is candidate *and* voter to anchor
                        assert anchorNode.voters().get(n).name().equals( candidateDiff._get( "bUsername" ));
                        rel = REL_TIGHT_CYCLE;
                    }
                    else rel = REL_CANDIDATE;
                    candidateDiff.init( candidateDiffKey, rel, /*sec*/-1, candidateNode, pollName,
                      /*isSelectandB*/false );
                }
                else assert false;
            }
        }
        if( tightCyclerPosition == 'a' ) // candidate (a) anchor (b)
        {
            candidateDiff = voterDiffBoard[candidateNode.dartSector()-1];
              // is voter *and* candidate to anchor
            assert candidateNode.name().equals( candidateDiff._get( "aUsername" ));
            candidateDiff.init2_rel( REL_TIGHT_CYCLE, /*sec*/-1 );
        }
        shadows = new DifferenceShadows( count, anchorNode, voterDiffBoard, peers, peerDiffBoard,
          candidateNode, candidateDiff, referrer );
        stage.setDifferencesShadowed();
        NodeV.setDeselectionGuard( "Lax" );
          // Allow click to deselect any node in vote track.  With default actor set,
          // 'Default' would be same as 'Lax' but slightly slower.
        final DifferenceShadowsV shadowsV = new DifferenceShadowsV( shadows, stage );
        try { shadowsV.init(); }
        catch( final DifferenceShadowsV.Warning warning ) { stage.addWarning( warning.toString() ); }
        new ShadowLight( shadows, shadowsV, stage );
        final VoteTrack voteTrack = VoteTrack.i( stage ); // if any is staged
        if( voteTrack != null )
        {
            final RootPanel body = RootPanel.get();
            body.addStyleName( "bottomStaged" );
            final FlowPanel stageV = new FlowPanel();
            body.add( stageV );
            stageV.getElement().setId( "StageV-bottom" );
            stageV.addStyleName( "staged" );
            stageV.add( new VoteTrackV( voteTrack, /*isBottomFixed*/true ));
        }
    }


        private int onSuccess_init( final JsArray<CountNodeJS> nodeBoard, final int start,
          final ResponseABReader res, final ShadowedDiff[] diffBoard, final char rel )
        {
            assert !res.isEnd();
            int n = start;
            for(; n < DART_SECTOR_MAX; ++n ) // possible for guard to fail immediately
            {
                final CountNodeJS node = nodeBoard.get( n );
                if( node == null ) continue;

                final String nodeName = node.name();
                if( nodeName.equals( anchorName )) continue;
                  // only possible in peer board, expect no diff vs. self

                res.verifyMatch( nodeName );
                final ShadowedDiff diff = res.diffOrNull;
                if( diff != null )
                {
                    assert res.keyOrNull != null;
                    diff.init( res.keyOrNull, rel, /*sec*/n+1, node, pollName,
                      /*isSelectandB*/res.isAResponse );
                    diffBoard[n] = diff;
                }
                if( res.hasNext() ) res.next();
                else
                {
                    res.end();
                    return n + 1;
                }
            }
            return n;
        }



   // - C o u n t - C a c h e - L i s t e n e r ------------------------------------------


    public void votersSet( final CountNodeEvent e )
    {
        if( hR == null ) return; // initialization is already complete

        final CountJS count = e.count();
        if( !count.pollName().equals( pollName )) return;

        final CountNodeJS node = e.node();
        final String name = node.name();
        if( anchorNode == null )
        {
            if( name.equals( anchorName ))
            {
                anchorNode = node;
                DifferenceShadower.this.count = count;
                if( toShadow() ) shadow(); // else await candidateNode
            }
        }
        else if( name.equals( anchorNode.candidateName() ))
        {
            candidateNode = node;
            shadow();
        }
    }



//// P r i v a t e ///////////////////////////////////////////////////////////////////////



    private String anchorName; // final after stageReady



    private CountNodeJS anchorNode; // final after votersSet, complete with voters



    private CountNodeJS candidateNode; // final after votersSet, complete with voters



    private CountJS count; // final after votersSet



    private HandlerRegistration hR; // final after stageReady, nulled if/when shadow



    private JsArray<CountNodeJS> peers; // final after shadow



    private String pollName; // final after stageReady



    private TheatrePage referrer; // if resolved before shadows constructed



    private String requestLoc; // final after initialized



    private void shadow()
    {
        hR.removeHandler();
        hR = null;

      // Request differences from server.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        final StringBuilder a = new StringBuilder(); // a positioned authors
        AuthorAppender authorAppender;
        if( candidateNode == null )
        {
            authorAppender = new AuthorAppender( anchorNode.voters() );
            if( anchorNode.isCandidate() ) peers = count.baseCandidates();
              // anchor is root candidate
        }
        else
        {
            authorAppender = new AuthorAppender( anchorNode.voters() )
            {
                private final int nTightCycler =
                  candidateNode.isCycler() && anchorName.equals(candidateNode.candidateName())?
                    /*voter index 0-19*/candidateNode.dartSector() - 1: /*none expected*/-1;
                @Override void append( final StringBuilder sB, final int n, final CountNodeJS node )
                {
                    if( n == nTightCycler )
                    {
                        final String candidateName = candidateNode.name();
                        tightCyclerPosition = DiffKey.isDartOrdered( anchorNode, anchorName,
                          candidateNode, candidateNode.name() )? 'b':'a';
                        if( tightCyclerPosition == 'b' ) return; /* tight cycler is both
                          voter and candidate to anchor.  This one happens to be ordered
                          like a candidate and is therefore appended directly below */
                    }

                    super.append( sB, n, node );
                }
            };
            peers = candidateNode.voters();
        }
        authorAppender.append( a, 0, DART_SECTOR_MAX );
        final StringBuilder b; // b positioned authors
        final int bN;
        if( peers == null ) // anchor is orphan
        {
            peers = JavaScriptObject.createArray( DART_SECTOR_MAX ).cast();
            b = null;
            bN = 0;
        }
        else
        {
            b = new StringBuilder();
            authorAppender = new AuthorAppender( peers );
            final int sectorC = anchorNode.dartSector(); // zero (mosquito) or 1-20
            authorAppender.append( a, 0, sectorC - 1 );
            authorAppender.append( b, sectorC, DART_SECTOR_MAX );
            if( candidateNode != null && tightCyclerPosition != 'a' )
            {
                b.append( candidateNode.name() );
            }
            bN = b.length();
        }
        final int aN = a.length();
        if( aN > 0 ) a.deleteCharAt( aN - 1 ); // trailing ( left by init_appendAuthors
        else if( bN == 0 ) return; // nothing to request

        final StringBuilder c = GWTX.stringBuilderClear();
        c.append( App.getServletContextLocation() );
        c.append( "/wap?wCall=dDiff&dPoll=" ).append( URL.encodeQueryString( pollName ));
        c.append( "&dAnchor=" ).append( URL.encodeQueryString( anchorName ));
        if( aN > 0 ) c.append( "&dA=" ).append( URL.encodeQueryString( a.toString() ));
        if( bN > 0 ) c.append( "&dB=" ).append( URL.encodeQueryString( b.toString() ));
        c.append( "&dPairData&wNonce=" ).append( URLX.serialNonce() );
          // per a.web.wap.ResponseConfiguration.headNoCache()
        requestLoc = c.toString();
        App.i().jsonpWAP().requestObject( requestLoc, DifferenceShadower.this );
        // continues at onFailure or onSuccess
    }



    private DifferenceShadows shadows; // set in onSuccess



    private void stageReady( final Stage stage ) // after GWTInitCallback methods called and defaults set
    {
      // Abort if not position draft in correct form.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        anchorName = stage.getDefaultActorName();
        pollName = stage.getDefaultPollName();
        if( anchorName == null || pollName == null ) return; // cannot be position draft

        final JavaScriptObject window = WindowX.js();
        final String wgAction = window._get( "wgAction" );
        if( !"view".equals(wgAction) && !"purge".equals(wgAction) ) return;
          // user not viewing text, but editing, looking at history, or something like that

        final String oldid = Window.Location.getParameter( "oldid" );
        if( oldid != null )
        {
            final int rev = Integer.parseInt( oldid );
            if( rev != window._getInt( "wgCurRevisionId" )) return; // not the current rev
        }

        final Element voDraft = Document.get().getElementById( "voDraft" );
        if( voDraft == null ) // not a remote draft, but maybe a local draft
        {
            final int wgNamespaceNumber = window._getInt( "wgNamespaceNumber" );
            if( wgNamespaceNumber != 2 ) return; // not a user page, cannot be a local draft

            final JsArrayString categories = window._get( "wgCategories" );
            if( categories == null ) return; // maybe impossible

            for( int c = categories.length() - 1;; --c )
            {
                if( c < 0 ) return; // not categorized as a draft, cannot be a local draft

                final String category = categories.get( c );
                if( "Draft".equals( category )) break;
            }
        }

      // Wait for anchoring count nodes to be fetched and cached.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        assert VoteTrack.i(stage) != null: "stage has node requestor to trigger votersSet";
        hR = GWTX.i().bus().addHandlerToSource( CountCacheEvent.TYPE, /*source*/CountCache.i(),
          DifferenceShadower.this );
        // may eventually continue at votersSet
    }



    private char tightCyclerPosition; // set 'a' or 'b' if anchor's candidate also anchor's voter



    private boolean toShadow()
    {
        if( anchorNode == null ) return false;

        if( candidateNode != null ) return true; // got both nodes

        return !anchorNode.isVoter(); // expect no candidate if no vote cast
    }



   // ====================================================================================


    private static class AuthorAppender
    {

        AuthorAppender( JsArray<CountNodeJS> _nodeBoard ) { nodeBoard = _nodeBoard; }


        final void append( final StringBuilder b, final int first, final int end/*exclusive*/ )
        {
            for( int n = first; n < end; ++n )
            {
                final CountNodeJS node = nodeBoard.get( n );
                if( node != null ) append( b, n, node );
            }
        }


        void append( final StringBuilder b, final int n, final CountNodeJS node )
        {
            b.append( node.name() );
            b.append( '(' );
        }


        private final JsArray<CountNodeJS> nodeBoard;

    }



   // ====================================================================================


    private static final class ResponseABReader
    {

        ResponseABReader( final JavaScriptObject d, final String specName )
        {
            if( "A".equals( specName ))
            {
                isAResponse = true;
                usernamePropertyName = "aUsername";
            }
            else
            {
                assert "B".equals( specName );
                isAResponse = false;
                usernamePropertyName = "bUsername";
            }
            diffMap = d._get( "diff" );
            diffX = d._get( "diffX" );
            keys = diffX._get( specName );
            if( keys == null ) kLast = -1;
            else kLast = keys.length() - 1;
        }


        ShadowedDiff diffOrNull;

        private final JsMap<ShadowedDiff> diffMap;

        private final JavaScriptObject diffX;


        void end() // go beyond last
        {
            k = kLast + 1;
            diffOrNull = null;
        }


        boolean hasNext() { return k < kLast; }


        final boolean isAResponse;


        boolean isEnd() { return k > kLast; } // beyond last


        private int k = -1;

        String keyOrNull; // null iff diffOrNull is null

        private final JsArrayString keys;

        private final int kLast;


        void next()
        {
            assert hasNext();
            set( k + 1 );
        }


        private void set( int _k )
        {
            k = _k;
            keyOrNull = keys.get( k );
            if( keyOrNull == null ) diffOrNull = null;
            else diffOrNull = diffMap.get( keyOrNull );
        }


        private final String usernamePropertyName;


        void verifyMatch( final String usernameExpected )
        {
            if( diffOrNull == null ) return; /* maybe no diff available, or maybe excluded
              because of tightCyclerPosition */

            final String name = diffOrNull._get( usernamePropertyName );
            if( !usernameExpected.equals( name ))
            {
                throw new IllegalStateException( "expecting username '" + usernameExpected
                  + "', DiffWAP responded with '" + name + "'" );
            }
        }

    }


}