package votorola.s.gwt.stage.link; // 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.user.client.rpc.AsyncCallback;
import com.google.web.bindery.event.shared.HandlerRegistration;
import java.util.*;
import java.util.regex.*;
import votorola.a.count.gwt.*;
import votorola.a.diff.*;
import votorola.a.web.gwt.*;
import votorola.g.*;
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.DifferenceLight.MAX_DIFFS;
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 targeter for a difference link with support for nominal targeting.  Nominal
  * targeting specifies differences in terms of poll and author names (aAuthor and bAuthor
  * in {@linkplain votorola.s.wic.diff.WP_D WP_D}) as opposed to the revision numbers of
  * draft pages.  A nominal target always refers to the latest difference of the two
  * authors.  This particular targeter is restricted to differences between the
  * {@linkplain VoteTrack#anchor() anchor} and his/her direct {@linkplain
  * votorola.a.count.XCastRelation cast relations}.  It nominally targets the link
  * whenever the following conditions are met:<ul>
  *
  * <li>The link would not otherwise be targeted to an actual page location, either
  * because there is no {@linkplain Stage#getDifference staged difference}, the staged
  * difference is already nominal, or it is already shown in the present page (as for
  * instance in the {@linkplain votorola.s.wic.diff.WP_D bridge}).</li>
  *
  * <li>A {@linkplain Stage#getPollName poll is staged}.</li>
  *
  * <li>An {@linkplain Stage#getActorName actor is staged} who is a direct cast relation
  * within that poll of the anchor.</li>
  *
  * <li>Both anchor and actor have formal positions in the poll and so assumedly <a
  * href='http://reluk.ca/w/Category:Draft' target='_top'>drafts</a>.</li></ul>
  *
  * <p>Under these conditions, the link is nominally targeted and the stage is set to
  * {@linkplain #NOMINAL_DIFF NOMINAL_DIFF}.</p>
  */
public final class NominalDifferenceTargeter extends LinkTrackV.TargeterN
  implements AsyncCallback<JavaScriptObject>, ChangeHandler
{


    /** Does nothing itself but the call forces static initialization of this class.
      */
    public static void forceInitClass() {}



    /** Creates a NominalDifferenceTargeter.  Create at most one for the entire life of the
      * document, as currently it does not unregister its listeners or otherwise clean up
      * after itself.
      */
    NominalDifferenceTargeter( final LinkTrackV trackV, final AnchorElement diffLink )
    {
        trackV.super( diffLink );
        final Stage stage = Stage.i();
        voteTrack = VoteTrack.i( stage );
        if( voteTrack == null ) throw new IllegalStateException( "no vote track" );

        new NominalDifferenceLight( stage, NominalDifferenceTargeter.this );
        GWTX.i().bus().addHandlerToSource( Change.TYPE, /*source*/voteTrack,
          NominalDifferenceTargeter.this ); // no need to unregister, registry does not outlive this listener
    }



    private static NominalDifferenceTargeter instance;

        {
            if( instance != null ) throw new IllegalStateException();
              // need multiple instances?  factor out diffMap and its expensive populating
              // code as a singleton for use by the NominalDifferenceLight singleton

            instance = NominalDifferenceTargeter.this;
        }



   // ------------------------------------------------------------------------------------


    /** The map of lightable differences keyed by author name.  Changes to map state are
      * signalled by {@linkplain Change change events} on the {@linkplain GWTX#bus() bus}.
      * The event source is {@linkplain #diffMapS diffMapS}.
      */
    Map<String,LightableDifference1> diffMap() { return diffMap; }


        private final Map<String,LightableDifference1> diffMap =  // all but the anchor
          new HashMap<String,LightableDifference1>(
            /*init capacity*/(int)((MAX_DIFFS + 1) / 0.75f) + 1 );


        private void diffMap_clear()
        {
            final boolean toFire = diffMap.size() > 0;
            diffMap.clear();
            if( toFire ) diffMap_fire();
        }


        private void diffMap_fire() { GWTX.i().bus().fireEventFromSource( new Change(), diffMapS ); }


        /** The source of change events for diffMap.
          */
        final Object diffMapS = new Object(); // workaround, grep CAES



    /** Answers whether nominal targeting is enabled.
      */
    public static boolean isEnabled() { return isEnabled; }


        private static boolean isEnabled;


        /** Enables nominal targeting.  Call this method from the global configuration
          * function {@linkplain StageMod voGWTConfig.s_gwt_stage} in this fashion:<pre
          *
         *>   s_gwt_stage_link_NominalDifferenceTargeter_setEnabled();</pre>
          */
        public static @GWTConfigCallback void setEnabled() { isEnabled = true; }


        private static native void exposeEnabled()
        /*-{
            $wnd.s_gwt_stage_link_NominalDifferenceTargeter_setEnabled = $entry(
              @votorola.s.gwt.stage.link.NominalDifferenceTargeter::setEnabled() );
        }-*/;


        static
        {
            assert StageMod.isForcedInit(): "forced init " + NominalDifferenceTargeter.class.getName();
            exposeEnabled();
        }



    /** The difference that is {@linkplain Stage#getDifference() staged} whenever the
      * difference link is nominally targeted.  The diff key is fabricated and very
      * unlikely to correspond to an actual difference in the wiki.
      */
    public static final DiffLook NOMINAL_DIFF;

        static
        {
            final String rS = Integer.toString( Integer.MAX_VALUE ); // unlikely rev
            final String veryUnlikelyKey = rS + '.' + rS + '.' + rS + '-' + rS + '.' + rS + '.' + rS;
            NOMINAL_DIFF = new DiffLook1( veryUnlikelyKey, /*selectand*/"a",
              /*toPersist*/false/*because weird and actor names effectively persist it anyway*/ );
        }



    @Override void retarget()
    {
        final Stage stage = Stage.i();
        final DiffLook look = stage.getDifference();
        final boolean wasNominal = look == NOMINAL_DIFF;
        boolean isNominal = false; // thus far
        String href = null;
        if( look != null && !wasNominal ) href = DifferenceTargeter.href( look );
          // assigns null if that page already showing
        if( href == null ) // either no diff is staged, or its page is already showing
        {
            final String actorName = stage.getActorName();
            if( actorName != null )
            {
                final CountNodeJS anchor = voteTrack.anchor();
                if( anchor != null )
                {
                    final String anchorName = anchor.name();
                    if( !anchorName.equals( actorName )) // then pair of authors is staged
                    {
                        final LightableDifference1 diff = diffMap.get( actorName );
                        if( diff != null )
                        {
                            isNominal = true;
                            final boolean isSelectandA = "a".equals( diff.selectand() );
                            final String aAuthor;
                            final String bAuthor; // correct order will prevent 2nd redirection
                            if( isSelectandA )
                            {
                                aAuthor = anchorName;
                                bAuthor = actorName;
                            }
                            else
                            {
                                aAuthor = actorName;
                                bAuthor = anchorName;
                            }
                            final StringBuilder b = GWTX.stringBuilderClear();
                            b.append( App.getServletContextLocation() ).append( "/w/D?" );
                            b.append(  "aAuthor=" ).append( URL.encodeQueryString( aAuthor ));
                            b.append( "&bAuthor=" ).append( URL.encodeQueryString( bAuthor ));
                            b.append( "&poll=" ).append( URL.encodeQueryString(
                              voteTrack.count().pollName() ));
                            if( isSelectandA ) b.append( "&s" ); // reversing it, so other selected
                            href = b.toString();
                        }
                    }
                }
            }
        }
        setTarget( adjustedTarget( href ));
        if( isNominal )
        {
            if( !wasNominal ) stage.setDifference( NOMINAL_DIFF );
        }
        else if( wasNominal ) stage.setDifference( null );
    }



    /** The staged instance of the vote track.
      */
    VoteTrack voteTrack() { return voteTrack; }


        private final VoteTrack voteTrack;



   // - 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_stage_link_NominalDifferenceTargeter_requestFail( x,
          onChange_requestLoc ));
    }



    public void onSuccess( final JavaScriptObject response )
    {
        diffMap_clear(); // in case of delayed response, at least clear previous response's authors
        try
        {
            final Board peers = voteTrack.peers();
            final Board voters = voteTrack.voters();
            if( !isVoteTrackReady( peers, voters )) return; // abort response, it's changing again

            JavaScriptObjectX pages = response._get( "query" );
            if( pages != null ) pages = pages._get( "pages" );
            if( pages == null )
            {
                Stage.i().addWarning( App.i().mesS().gwt_stage_link_NominalDifferenceTargeter_requestWarn(
                  "response has no 'pages'", onChange_requestLoc ));
                return;
            }

            final CountNodeJS anchor = voteTrack.anchor();
            if( anchor == null ) throw new IllegalStateException( "isVoteTrackReady and no anchor" );

            final String anchorName = anchor.name();
            boolean isAnchorAuthor = false; // till proven otherwise
            for( String p: pages._in() )
            {
                if( p.charAt(0) == '_' ) continue; // skip __gwt_ObjectId, seen on Chrome

                final JavaScriptObject page = pages._get( p );
                if( !page._hasProperty( "pageid" )) continue; // no position page

                final String pageName = page._getString( "title" );
                final MatchResult m = MediaWiki.parsePageNameS( pageName );
                if( m == null ) throw new IllegalStateException( "malformed page name" );

                final String authorName = m.group( 2 ); // in canonical form already, API does that
                if( authorName.equals( anchorName ))
                {
                    isAnchorAuthor = true;
                    continue;
                }

                final char rel;
                final int dartSector;
                final String selectand;
                final CountNodeJS candidate = voteTrack.candidate();
                if( candidate != null && candidate.name().equals( authorName ))
                {
                    if( voteTrack.hasTightCycle() )
                    {
                        rel = REL_TIGHT_CYCLE;
                        selectand = DiffKey.isDartOrdered( anchor, anchorName, candidate,
                          candidate.name() )? "a":"b";
                    }
                    else
                    {
                        rel = REL_CANDIDATE;
                        selectand = "a";
                    }
                    dartSector = -1; // not used
                }
                else
                {
                    CountNodeJS node;
                    node = onSuccess_find( authorName, voteTrack.voters() );
                    if( node != null )
                    {
                        rel = REL_VOTER;
                        selectand = "b";
                    }
                    else
                    {
                        node = onSuccess_find( authorName, voteTrack.peers() );
                        if( node != null )
                        {
                            rel = REL_CO;
                            selectand = DiffKey.isDartOrdered( anchor, anchorName, node,
                              node.name() )? "a":"b";
                        }
                        else // very unlikely
                        {
                            Stage.i().addWarning( App.i().mesS()
                                .gwt_stage_link_NominalDifferenceTargeter_requestWarn(
                             "author no longer in track, wiki responses maybe out of sequence",
                              onChange_requestLoc ));
                            continue; // hope for the best
                        }
                    }

                    dartSector = node.dartSector();
                }
                diffMap.put( authorName, new LightableDifference1( rel, dartSector, selectand,
                  authorName, voteTrack.count().pollName() ));
            }
            if( !isAnchorAuthor ) diffMap.clear(); /* undo because there can be no
              differences with anchor, after all.  No need to fire events or anything,
              this is a plain undo */
            else if( diffMap.size() > 0 ) diffMap_fire(); // because it changed
        }
        finally{ retarget(); }
    }


        private CountNodeJS onSuccess_find( final String authorName, final Board board )
        {
            CountNodeJS node;
            node = board.mosquito();
            if( node != null && node.name().equals( authorName )) return node;

            for( int s = 1; s <= DART_SECTOR_MAX; ++s )
            {
                node = board.node( s );
                if( node != null && node.name().equals( authorName )) return node;
            }

            return null;
        }



   // - C h a n g e - H a n d l e r - ----------------------------------------------------


    public void onChange( Change _e )
    {
        final Board peers = voteTrack.peers();
        final Board voters = voteTrack.voters();
        if( !isVoteTrackReady( peers, voters )) return; // wait for it

        final String pollName = voteTrack.count().pollName();
        final StringBuilder b = GWTX.stringBuilderClear();
        final PollwikiG wiki = App.i().pollwiki();
        onChange_append( b, voters, pollName, wiki );
        onChange_append( b, voteTrack.peers(), pollName, wiki );
        final CountNodeJS candidate = voteTrack.candidate();
        if( candidate != null )
        {
            if( voteTrack.hasTightCycle() ) assert b.length() > 0; // already appended as voter
            else onChange_append( b, candidate, pollName, wiki );
        }
        else if( b.length() == 0 ) return; // anchor is orphan
          // [orphan?  length zero seems impossible for an orphan, which would be appended
          // as mosquito in peer board]

        b.deleteCharAt( b.length() - 1 ); // remove trailing separator '|'
        b.insert( 0, "/api.php?titles=" ); // limit 50 titles, need 21 * 2 + 1 = 43 max
        b.insert( 0, PollwikiG.getScriptLocation() );
        b.append( "&action=query&format=json" );
        onChange_requestLoc = b.toString();
        App.i().jsonp().requestObject( onChange_requestLoc, NominalDifferenceTargeter.this );
        // continues at onFailure or onSuccess
    }


        private static void onChange_append( final StringBuilder b, final Board board,
          final String pollName, final PollwikiG wiki )
        {
            CountNodeJS node;
            node = board.mosquito();
            if( node != null ) onChange_append( b, node, pollName, wiki );
            for( int s = 1; s <= DART_SECTOR_MAX; ++s )
            {
                node = board.node( s );
                if( node != null ) onChange_append( b, node, pollName, wiki );
            }
        }


        private static void onChange_append( final StringBuilder b, final CountNodeJS node,
          final String pollName, final PollwikiG wiki )
        {
            b.append( URL.encodeQueryString( wiki.positionPageName( node.name(), pollName )));
            b.append( '|' );
        }


        private String onChange_requestLoc;



   // - P r o p e r t y - C h a n g e - H a n d l e r ------------------------------------


    public void onPropertyChange( final PropertyChange e )
    {
        final String pName = e.propertyName();
        if( pName.equals("actorName") || pName.equals("difference") ) coalescer.schedule();
        else if( pName.equals( "pollName" ))
        {
            diffMap_clear(); // till sync with vote track completes
            coalescer.schedule();
        }
        // eventually continues at retarget if coalescer called
    }



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


    private static boolean isVoteTrackReady( final Board peers, final Board voters )
    {
        return voters.isEnabled()/*anchored*/ && peers.isEnabled()/*full track*/;
    }


}