// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.ui.liveedit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.chromium.debug.core.model.PushChangesPlan;
import org.chromium.debug.core.util.ScriptTargetMapping;
import org.chromium.debug.ui.WizardUtils.LogicBasedWizard;
import org.chromium.debug.ui.WizardUtils.PageElements;
import org.chromium.debug.ui.WizardUtils.PageElementsFactory;
import org.chromium.debug.ui.WizardUtils.PageImpl;
import org.chromium.debug.ui.WizardUtils.WizardFinisher;
import org.chromium.debug.ui.WizardUtils.WizardLogic;
import org.chromium.debug.ui.WizardUtils.WizardPageSet;
import org.chromium.debug.ui.actions.ChooseVmControl;
import org.chromium.debug.ui.liveedit.LiveEditResultDialog.Input;
import org.chromium.debug.ui.liveedit.LiveEditResultDialog.SingleInput;
import org.chromium.sdk.CallbackSemaphore;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.UpdatableScript;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.wizard.IWizard;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;


/**
 * A wizard that pushes script changes to V8 VM (LiveEdit); it also lets user choose target VM(s),
 * review changes in script text, review which function will be patched.
 */
public class PushChangesWizard {

  public static void start(final List<? extends ScriptTargetMapping> filePairs, Shell shell) {
    // Create pages.
    final PageImpl<ChooseVmPageElements> chooseVmPage = new PageImpl<ChooseVmPageElements>(
        "choose VM", //$NON-NLS-1$
        CHOOSE_VM_PAGE_FACTORY,
        Messages.PushChangesWizard_CHOOSE_VM,
        Messages.PushChangesWizard_CHOOSE_VM_DESCRIPTION);

    final PageImpl<V8PreviewPageElements> v8PreviewPage = new PageImpl<V8PreviewPageElements>(
        "v8 preview", //$NON-NLS-1$
        V8_PREVIEW_PAGE_FACTORY,
        Messages.PushChangesWizard_V8_PREVIEW,
        Messages.PushChangesWizard_V8_PREVIEW_DESCRIPTION);
    final PageImpl<PageElements> multipleVmStubPage = new PageImpl<PageElements>(
        "multiple vm", //$NON-NLS-1$
        MULTIPLE_VM_STUB_PAGE_FACTORY,
        Messages.PushChangesWizard_MULTIPLE_VM,
        Messages.PushChangesWizard_MULTIPLE_VM_DESCRIPTION);

    final PageSet pageSet = new PageSet() {
      public List<? extends PageImpl<?>> getAllPages() {
        return Arrays.<PageImpl<?>>asList(chooseVmPage, v8PreviewPage,
            multipleVmStubPage);
      }
      public PageImpl<ChooseVmPageElements> getChooseVmPage() {
        return chooseVmPage;
      }
      public PageImpl<V8PreviewPageElements> getV8PreviewPage() {
        return v8PreviewPage;
      }
      public PageImpl<PageElements> getMultipleVmStubPage() {
        return multipleVmStubPage;
      }
      public WizardLogic createLogic(final LogicBasedWizard wizardImpl) {
        WizardLogicBuilder logicBuilder = new WizardLogicBuilder(this, wizardImpl);
        return logicBuilder.create(filePairs);
      }
    };

    // Start wizard engine.
    LogicBasedWizard wizard = new LogicBasedWizard(pageSet);
    wizard.setWindowTitle(Messages.PushChangesWizard_TITLE);
    WizardDialog wizardDialog = new WizardDialog(shell, wizard);
    wizardDialog.open();
  }

  /**
   * An access to all wizard pages.
   */
  interface PageSet extends WizardPageSet {
    PageImpl<ChooseVmPageElements> getChooseVmPage();
    PageImpl<V8PreviewPageElements> getV8PreviewPage();
    PageImpl<PageElements> getMultipleVmStubPage();
  }

  ///  Interfaces that link logic with UI controls on each pages.

  interface ChooseVmPageElements extends PageElements {
    ChooseVmControl.Logic getChooseVm();
  }

  interface V8PreviewPageElements extends PageElements {
    LiveEditDiffViewer getPreviewViewer();
  }

  ///  Factories that create UI controls for pages.

  private static final PageElementsFactory<ChooseVmPageElements> CHOOSE_VM_PAGE_FACTORY =
      new PageElementsFactory<ChooseVmPageElements>() {
    public ChooseVmPageElements create(Composite parent) {
      final ChooseVmControl.Logic chooseVm = ChooseVmControl.create(parent);
      chooseVm.getControl().setLayoutData(new GridData(GridData.FILL_BOTH));

      return new ChooseVmPageElements() {
        public ChooseVmControl.Logic getChooseVm() {
          return chooseVm;
        }
        public Control getMainControl() {
          return chooseVm.getControl();
        }
      };
    }
  };

  private static final PageElementsFactory<V8PreviewPageElements> V8_PREVIEW_PAGE_FACTORY =
      new PageElementsFactory<V8PreviewPageElements>() {
    public V8PreviewPageElements create(Composite parent) {
      final Composite page = new Composite(parent, SWT.NONE);
      page.setLayout(new GridLayout(1, false));
      LiveEditDiffViewer.Configuration configuration =
          new LiveEditDiffViewer.Configuration() {
            public String getNewLabel() {
              return Messages.PushChangesWizard_CHANGED_SCRIPT;
            }
            public String getOldLabel() {
              return Messages.PushChangesWizard_SCRIPT_IN_VM;
            }
            public boolean oldOnLeft() {
              return false;
            }
      };
      final LiveEditDiffViewer viewer = LiveEditDiffViewer.create(page, configuration);

      return new V8PreviewPageElements() {
        public Control getMainControl() {
          return page;
        }
        public LiveEditDiffViewer getPreviewViewer() {
          return viewer;
        }
      };
    }
  };

  private static final PageElementsFactory<PageElements> MULTIPLE_VM_STUB_PAGE_FACTORY =
      new PageElementsFactory<PageElements>() {
        public PageElements create(Composite parent) {
          final Label label = new Label(parent, 0);

          return new PageElements() {
            public Control getMainControl() {
              return label;
            }
          };
        }
      };

  interface FinisherDelegate {
    LiveEditResultDialog.Input run(IProgressMonitor monitor);
  }

  static class FinisherImpl implements WizardFinisher {
    private final FinisherDelegate delegate;
    FinisherImpl(FinisherDelegate delegate) {
      this.delegate = delegate;
    }
    public boolean performFinish(IWizard wizard, IProgressMonitor monitor) {
      LiveEditResultDialog.Input dialogInput = delegate.run(monitor);
      LiveEditResultDialog dialog =
          new LiveEditResultDialog(wizard.getContainer().getShell(), dialogInput);
      dialog.open();
      return true;
    }
  }

  /**
   * A callback that gets called when user presses 'finish' and a single VM is selected.
   */
  static class SingleVmFinisher implements FinisherDelegate {
    private final PushChangesPlan pushChangesPlan;

    public SingleVmFinisher(PushChangesPlan pushChangesPlan) {
      this.pushChangesPlan = pushChangesPlan;
    }
    public Input run(IProgressMonitor monitor) {
      return performSingleVmUpdate(pushChangesPlan, monitor);
    }
  }

  /**
   * A callback that gets called when user presses 'finish' and several VMs are selected.
   * It performs update and opens result dialog window.
   */
  static class MultipleVmFinisher implements FinisherDelegate {
    private final List<PushChangesPlan> targets;
    public MultipleVmFinisher(List<PushChangesPlan> targets) {
      this.targets = targets;
    }

    /**
     * Performs updates for each VM and opens dialog window with composite result.
     */
    public Input run(IProgressMonitor monitor) {
      monitor.beginTask(null, targets.size());
      final List<LiveEditResultDialog.SingleInput> results =
          new ArrayList<LiveEditResultDialog.SingleInput>();
      for (PushChangesPlan plan : targets) {
        LiveEditResultDialog.SingleInput dialogInput =
            performSingleVmUpdate(plan, new SubProgressMonitor(monitor, 1));
        results.add(dialogInput);
      }
      monitor.done();

      final LiveEditResultDialog.MultipleResult multipleResult =
          new LiveEditResultDialog.MultipleResult() {
            public List<? extends SingleInput> getList() {
              return results;
            }
      };

      return new LiveEditResultDialog.Input() {
        public <RES> RES accept(LiveEditResultDialog.InputVisitor<RES> visitor) {
          return visitor.visitMultipleResult(multipleResult);
        }
      };
    }
  }

  /**
   * Performs update to a VM and returns result in form of dialog window input.
   */
  private static LiveEditResultDialog.SingleInput performSingleVmUpdate(
      final PushChangesPlan changesPlan, IProgressMonitor monitor) {
    final LiveEditResultDialog.SingleInput[] input = { null };

    UpdatableScript.UpdateCallback callback = new UpdatableScript.UpdateCallback() {
      public void failure(String message) {
        String text = NLS.bind("Failure: {0}", message);
        input[0] = LiveEditResultDialog.createTextInput(text, changesPlan);
      }
      public void success(Object report,
          final UpdatableScript.ChangeDescription changeDescription) {
        if (changeDescription == null) {
          input[0] = LiveEditResultDialog.createTextInput(
              Messages.PushChangesWizard_EMPTY_CHANGE, changesPlan);
        } else {
          final String oldScriptName = changeDescription.getCreatedScriptName();
          final LiveEditResultDialog.OldScriptData oldScriptData;
          if (oldScriptName == null) {
            oldScriptData = null;
          } else {
            final LiveEditDiffViewer.Input previewInput =
                PushResultParser.createViewerInput(changeDescription, changesPlan, false);
            oldScriptData = new LiveEditResultDialog.OldScriptData() {
              public LiveEditDiffViewer.Input getScriptStructure() {
                return previewInput;
              }
              public String getOldScriptName() {
                return oldScriptName;
              }
            };
          }
          final LiveEditResultDialog.SuccessResult successResult =
              new LiveEditResultDialog.SuccessResult() {
                public LiveEditResultDialog.OldScriptData getOldScriptData() {
                  return oldScriptData;
                }
                public boolean hasDroppedFrames() {
                  return changeDescription.isStackModified();
                }
          };
          input[0] = new LiveEditResultDialog.SingleInput() {
            public <RES> RES accept(LiveEditResultDialog.InputVisitor<RES> visitor) {
              return acceptSingle(visitor);
            }
            public <RES> RES acceptSingle(LiveEditResultDialog.SingleInputVisitor<RES> visitor) {
              return visitor.visitSuccess(successResult);
            }
            public ScriptTargetMapping getFilePair() {
              return changesPlan.getScriptTargetMapping();
            }
          };
        }
      }
    };

    CallbackSemaphore syncCallback = new CallbackSemaphore();
    RelayOk relayOk = changesPlan.execute(false, callback, syncCallback);
    syncCallback.acquireDefault(relayOk);

    monitor.done();

    return input[0];
  }
}
