/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed 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.
 */

package com.android.sdkuilib.internal.widgets;

import com.android.SdkConstants;
import com.android.io.FileWrapper;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISystemImage;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.avd.AvdManager.AvdConflict;
import com.android.sdklib.internal.avd.HardwareProperties;
import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import com.android.sdkuilib.ui.GridDialog;
import com.android.utils.ILogger;
import com.android.utils.Pair;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ComboBoxCellEditor;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Text;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.regex.Matcher;

/**
 * AVD creation or edit dialog.
 *
 * TODO:
 * - use SdkTargetSelector instead of Combo
 * - tooltips on widgets.
 *
 */
final class LegacyAvdEditDialog extends GridDialog {

    private final AvdManager mAvdManager;
    private final TreeMap<String, IAndroidTarget> mCurrentTargets =
        new TreeMap<String, IAndroidTarget>();

    private final Map<String, HardwareProperty> mHardwareMap;
    private final Map<String, String> mProperties = new HashMap<String, String>();
    // a list of user-edited properties.
    private final ArrayList<String> mEditedProperties = new ArrayList<String>();
    private final ImageFactory mImageFactory;
    private final ILogger mSdkLog;
    /**
     * The original AvdInfo if we're editing an existing AVD.
     * Null when we're creating a new AVD.
     */
    private final AvdInfo mEditAvdInfo;

    private Text mAvdName;
    private Combo mTargetCombo;

    private Combo mAbiTypeCombo;
    private String mAbiType;

    private Button mSdCardSizeRadio;
    private Text mSdCardSize;
    private Combo mSdCardSizeCombo;

    private Text mSdCardFile;
    private Button mBrowseSdCard;
    private Button mSdCardFileRadio;

    private Button mSnapshotCheck;

    private Button mSkinListRadio;
    private Combo mSkinCombo;

    private Button mSkinSizeRadio;
    private Text mSkinSizeWidth;
    private Text mSkinSizeHeight;

    private TableViewer mHardwareViewer;
    private Button mDeleteHardwareProp;

    private Button mForceCreation;
    private Button mOkButton;
    private Label mStatusIcon;
    private Label mStatusLabel;
    private Composite mStatusComposite;

    /**
     * {@link VerifyListener} for {@link Text} widgets that should only contains numbers.
     */
    private final VerifyListener mDigitVerifier = new VerifyListener() {
        @Override
        public void verifyText(VerifyEvent event) {
            int count = event.text.length();
            for (int i = 0 ; i < count ; i++) {
                char c = event.text.charAt(i);
                if (c < '0' || c > '9') {
                    event.doit = false;
                    return;
                }
            }
        }
    };

    /**
     * Callback when the AVD name is changed.
     * When creating a new AVD, enables the force checkbox if the name is a duplicate.
     * When editing an existing AVD, it's OK for the name to match the existing AVD.
     */
    private class CreateNameModifyListener implements ModifyListener {
        @Override
        public void modifyText(ModifyEvent e) {
            String name = mAvdName.getText().trim();
            if (mEditAvdInfo == null || !name.equals(mEditAvdInfo.getName())) {
                // Case where we're creating a new AVD or editing an existing one
                // and the AVD name has been changed... check for name uniqueness.

                Pair<AvdConflict, String> conflict = mAvdManager.isAvdNameConflicting(name);
                if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) {
                    // If we're changing the state from disabled to enabled, make sure
                    // to uncheck the button, to force the user to voluntarily re-enforce it.
                    // This happens when editing an existing AVD and changing the name from
                    // the existing AVD to another different existing AVD.
                    if (!mForceCreation.isEnabled()) {
                        mForceCreation.setEnabled(true);
                        mForceCreation.setSelection(false);
                    }
                } else {
                    mForceCreation.setEnabled(false);
                    mForceCreation.setSelection(false);
                }
            } else {
                // Case where we're editing an existing AVD with the name unchanged.

                mForceCreation.setEnabled(false);
                mForceCreation.setSelection(false);
            }
            validatePage();
        }
    }

    /**
     * {@link ModifyListener} used for live-validation of the fields content.
     */
    private class ValidateListener extends SelectionAdapter implements ModifyListener {
        @Override
        public void modifyText(ModifyEvent e) {
            validatePage();
        }

        @Override
        public void widgetSelected(SelectionEvent e) {
            super.widgetSelected(e);
            validatePage();
        }
    }

    /**
     * Creates the dialog. Caller should then use {@link Window#open()} and
     * refresh if the status is {@link Window#OK}.
     *
     * @param parentShell The parent shell.
     * @param avdManager The existing {@link AvdManager} to use. Must not be null.
     * @param imageFactory An existing {@link ImageFactory} to use. Must not be null.
     * @param log An existing {@link ILogger} where output will go. Must not be null.
     * @param editAvdInfo An optional {@link AvdInfo}. When null, the dialog is used
     *   to create a new AVD. When non-null, the dialog is used to <em>edit</em> this AVD.
     */
    protected LegacyAvdEditDialog(Shell parentShell,
            AvdManager avdManager,
            ImageFactory imageFactory,
            ILogger log,
            AvdInfo editAvdInfo) {
        super(parentShell, 2, false);
        mAvdManager = avdManager;
        mImageFactory = imageFactory;
        mSdkLog = log;
        mEditAvdInfo = editAvdInfo;

        File hardwareDefs = null;

        SdkManager sdkMan = avdManager.getSdkManager();
        if (sdkMan != null) {
            String sdkPath = sdkMan.getLocation();
            if (sdkPath != null) {
                hardwareDefs = new File (sdkPath + File.separator +
                        SdkConstants.OS_SDK_TOOLS_LIB_FOLDER, SdkConstants.FN_HARDWARE_INI);
            }
        }

        if (hardwareDefs == null) {
            log.error(null, "Failed to load file %s from SDK", SdkConstants.FN_HARDWARE_INI);
            mHardwareMap = new HashMap<String, HardwareProperty>();
        } else {
            mHardwareMap = HardwareProperties.parseHardwareDefinitions(
                hardwareDefs, null /*sdkLog*/);
        }
    }

    @Override
    public void create() {
        super.create();

        Point p = getShell().getSize();
        if (p.x < 400) {
            p.x = 400;
        }
        getShell().setSize(p);
    }

    @Override
    protected Control createContents(Composite parent) {
        Control control = super.createContents(parent);
        getShell().setText(mEditAvdInfo == null ? "Create new Android Virtual Device (AVD)"
                                                : "Edit Android Virtual Device (AVD)");

        mOkButton = getButton(IDialogConstants.OK_ID);

        fillExistingAvdInfo();
        validatePage();

        return control;
    }

    @Override
    public void createDialogContent(final Composite parent) {
        GridData gd;
        GridLayout gl;

        Label label = new Label(parent, SWT.NONE);
        label.setText("Name:");
        String tooltip = "Name of the new Android Virtual Device";
        label.setToolTipText(tooltip);

        mAvdName = new Text(parent, SWT.BORDER);
        mAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mAvdName.addModifyListener(new CreateNameModifyListener());
        mAvdName.setToolTipText(tooltip);

        label = new Label(parent, SWT.NONE);
        label.setText("Target:");
        tooltip = "The version of Android to use in the virtual device";
        label.setToolTipText(tooltip);

        mTargetCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
        mTargetCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mTargetCombo.setToolTipText(tooltip);
        mTargetCombo.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                super.widgetSelected(e);
                reloadSkinCombo();
                reloadAbiTypeCombo();
                validatePage();
            }
        });

        //ABI group
        label = new Label(parent, SWT.NONE);
        label.setText("CPU/ABI:");
        tooltip = "The CPU/ABI to use in the virtual device";
        label.setToolTipText(tooltip);

         mAbiTypeCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
         mAbiTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
         mAbiTypeCombo.setToolTipText(tooltip);
         mAbiTypeCombo.addSelectionListener(new SelectionAdapter() {
         @Override
         public void widgetSelected(SelectionEvent e) {
                     super.widgetSelected(e);
                     validatePage();
                 }
         });
         mAbiTypeCombo.setEnabled(false);

        // --- sd card group
        label = new Label(parent, SWT.NONE);
        label.setText("SD Card:");
        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
                false, false));

        final Group sdCardGroup = new Group(parent, SWT.NONE);
        sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        sdCardGroup.setLayout(new GridLayout(3, false));

        mSdCardSizeRadio = new Button(sdCardGroup, SWT.RADIO);
        mSdCardSizeRadio.setText("Size:");
        mSdCardSizeRadio.setToolTipText("Create a new SD Card file");
        mSdCardSizeRadio.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                boolean sizeMode = mSdCardSizeRadio.getSelection();
                enableSdCardWidgets(sizeMode);
                validatePage();
            }
        });

        ValidateListener validateListener = new ValidateListener();

        mSdCardSize = new Text(sdCardGroup, SWT.BORDER);
        mSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mSdCardSize.addVerifyListener(mDigitVerifier);
        mSdCardSize.addModifyListener(validateListener);
        mSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)");

        mSdCardSizeCombo = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY);
        mSdCardSizeCombo.add("KiB");
        mSdCardSizeCombo.add("MiB");
        mSdCardSizeCombo.add("GiB");
        mSdCardSizeCombo.select(1);
        mSdCardSizeCombo.addSelectionListener(validateListener);

        mSdCardFileRadio = new Button(sdCardGroup, SWT.RADIO);
        mSdCardFileRadio.setText("File:");
        mSdCardFileRadio.setToolTipText("Use an existing file for the SD Card");

        mSdCardFile = new Text(sdCardGroup, SWT.BORDER);
        mSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mSdCardFile.addModifyListener(validateListener);
        mSdCardFile.setToolTipText("File to use for the SD Card");

        mBrowseSdCard = new Button(sdCardGroup, SWT.PUSH);
        mBrowseSdCard.setText("Browse...");
        mBrowseSdCard.setToolTipText("Select the file to use for the SD Card");
        mBrowseSdCard.addSelectionListener(new SelectionAdapter() {
           @Override
            public void widgetSelected(SelectionEvent arg0) {
               onBrowseSdCard();
               validatePage();
            }
        });

        mSdCardSizeRadio.setSelection(true);
        enableSdCardWidgets(true);

        // --- snapshot group

        label = new Label(parent, SWT.NONE);
        label.setText("Snapshot:");
        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
            false, false));

        final Group snapshotGroup = new Group(parent, SWT.NONE);
        snapshotGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        snapshotGroup.setLayout(new GridLayout(3, false));

        mSnapshotCheck = new Button(snapshotGroup, SWT.CHECK);
        mSnapshotCheck.setText("Enabled");
        mSnapshotCheck.setToolTipText(
                "Emulator's state will be persisted between emulator executions");

        // --- skin group
        label = new Label(parent, SWT.NONE);
        label.setText("Skin:");
        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
                false, false));

        final Group skinGroup = new Group(parent, SWT.NONE);
        skinGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        skinGroup.setLayout(new GridLayout(4, false));

        mSkinListRadio = new Button(skinGroup, SWT.RADIO);
        mSkinListRadio.setText("Built-in:");
        mSkinListRadio.setToolTipText("Select an emulated screen size provided by the current Android target");
        mSkinListRadio.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                boolean listMode = mSkinListRadio.getSelection();
                enableSkinWidgets(listMode);
                validatePage();
            }
        });

        mSkinCombo = new Combo(skinGroup, SWT.READ_ONLY | SWT.DROP_DOWN);
        mSkinCombo.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
        gd.horizontalSpan = 3;
        mSkinCombo.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                // get the skin info
                loadSkin();
            }
        });

        mSkinSizeRadio = new Button(skinGroup, SWT.RADIO);
        mSkinSizeRadio.setText("Resolution:");
        mSkinSizeRadio.setToolTipText("Select a custom emulated screen size");

        mSkinSizeWidth = new Text(skinGroup, SWT.BORDER);
        mSkinSizeWidth.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mSkinSizeWidth.addVerifyListener(mDigitVerifier);
        mSkinSizeWidth.addModifyListener(validateListener);
        mSkinSizeWidth.setToolTipText("Width in pixels of the emulated screen size");

        new Label(skinGroup, SWT.NONE).setText("x");

        mSkinSizeHeight = new Text(skinGroup, SWT.BORDER);
        mSkinSizeHeight.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mSkinSizeHeight.addVerifyListener(mDigitVerifier);
        mSkinSizeHeight.addModifyListener(validateListener);
        mSkinSizeHeight.setToolTipText("Height in pixels of the emulated screen size");

        mSkinListRadio.setSelection(true);
        enableSkinWidgets(true);

        // --- hardware group
        label = new Label(parent, SWT.NONE);
        label.setText("Hardware:");
        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
                false, false));

        final Group hwGroup = new Group(parent, SWT.NONE);
        hwGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        hwGroup.setLayout(new GridLayout(2, false));

        createHardwareTable(hwGroup);

        // composite for the side buttons
        Composite hwButtons = new Composite(hwGroup, SWT.NONE);
        hwButtons.setLayoutData(new GridData(GridData.FILL_VERTICAL));
        hwButtons.setLayout(gl = new GridLayout(1, false));
        gl.marginHeight = gl.marginWidth = 0;

        Button b = new Button(hwButtons, SWT.PUSH | SWT.FLAT);
        b.setText("New...");
        b.setToolTipText("Add a new hardware property");
        b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        b.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent event) {
                HardwarePropertyChooser dialog = new HardwarePropertyChooser(parent.getShell(),
                        mHardwareMap, mProperties.keySet());
                if (dialog.open() == Window.OK) {
                    HardwareProperty choice = dialog.getProperty();
                    if (choice != null) {
                        mProperties.put(choice.getName(), choice.getDefault());
                        mHardwareViewer.refresh();
                    }
                }
            }
        });
        mDeleteHardwareProp = new Button(hwButtons, SWT.PUSH | SWT.FLAT);
        mDeleteHardwareProp.setText("Delete");
        mDeleteHardwareProp.setToolTipText("Delete the selected hardware property");
        mDeleteHardwareProp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mDeleteHardwareProp.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                ISelection selection = mHardwareViewer.getSelection();
                if (selection instanceof IStructuredSelection) {
                    String hwName = (String)((IStructuredSelection)selection).getFirstElement();
                    mProperties.remove(hwName);
                    mHardwareViewer.refresh();
                }
            }
        });
        mDeleteHardwareProp.setEnabled(false);

        // --- end hardware group

        mForceCreation = new Button(parent, SWT.CHECK);
        mForceCreation.setText("Override the existing AVD with the same name");
        mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD.");
        mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER,
                true, false, 2, 1));
        mForceCreation.setEnabled(false);
        mForceCreation.addSelectionListener(validateListener);

        // add a separator to separate from the ok/cancel button
        label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
        label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1));

        // add stuff for the error display
        mStatusComposite = new Composite(parent, SWT.NONE);
        mStatusComposite.setLayoutData(new GridData(GridData.FILL, GridData.CENTER,
                true, false, 3, 1));
        mStatusComposite.setLayout(gl = new GridLayout(2, false));
        gl.marginHeight = gl.marginWidth = 0;

        mStatusIcon = new Label(mStatusComposite, SWT.NONE);
        mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
                false, false));
        mStatusLabel = new Label(mStatusComposite, SWT.NONE);
        mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mStatusLabel.setText(" \n "); //$NON-NLS-1$

        reloadTargetCombo();
    }

    /**
     * Creates the UI for the hardware properties table.
     * This creates the {@link Table}, and several viewers ({@link TableViewer},
     * {@link TableViewerColumn}) and adds edit support for the 2nd column
     */
    private void createHardwareTable(Composite parent) {
        final Table hardwareTable = new Table(parent, SWT.SINGLE | SWT.FULL_SELECTION);
        GridData gd = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL);
        gd.widthHint = 200;
        gd.heightHint = 100;
        hardwareTable.setLayoutData(gd);
        hardwareTable.setHeaderVisible(true);
        hardwareTable.setLinesVisible(true);

        // -- Table viewer
        mHardwareViewer = new TableViewer(hardwareTable);
        mHardwareViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                // it's a single selection mode, we can just access the selection index
                // from the table directly.
                mDeleteHardwareProp.setEnabled(hardwareTable.getSelectionIndex() != -1);
            }
        });

        // only a content provider. Use viewers per column below (for editing support)
        mHardwareViewer.setContentProvider(new IStructuredContentProvider() {
            @Override
            public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
                // we can just ignore this. we just use mProperties directly.
            }

            @Override
            public Object[] getElements(Object arg0) {
                return mProperties.keySet().toArray();
            }

            @Override
            public void dispose() {
                // pass
            }
        });

        // -- column 1: prop abstract name
        TableColumn col1 = new TableColumn(hardwareTable, SWT.LEFT);
        col1.setText("Property");
        col1.setWidth(150);
        TableViewerColumn tvc1 = new TableViewerColumn(mHardwareViewer, col1);
        tvc1.setLabelProvider(new CellLabelProvider() {
            @Override
            public void update(ViewerCell cell) {
                String name = cell.getElement().toString();
                HardwareProperty prop = mHardwareMap.get(name);
                if (prop != null) {
                    cell.setText(prop.getAbstract());
                } else {
                    cell.setText(String.format("%1$s (Unknown)", name));
                }
            }
        });

        // -- column 2: prop value
        TableColumn col2 = new TableColumn(hardwareTable, SWT.LEFT);
        col2.setText("Value");
        col2.setWidth(50);
        TableViewerColumn tvc2 = new TableViewerColumn(mHardwareViewer, col2);
        tvc2.setLabelProvider(new CellLabelProvider() {
            @Override
            public void update(ViewerCell cell) {
                String value = mProperties.get(cell.getElement());
                cell.setText(value != null ? value : "");
            }
        });

        // add editing support to the 2nd column
        tvc2.setEditingSupport(new EditingSupport(mHardwareViewer) {
            @Override
            protected void setValue(Object element, Object value) {
                String hardwareName = (String)element;
                HardwareProperty property = mHardwareMap.get(hardwareName);
                int index;
                switch (property.getType()) {
                    case INTEGER:
                        mProperties.put((String)element, (String)value);
                        break;
                    case DISKSIZE:
                        if (HardwareProperties.DISKSIZE_PATTERN.matcher((String)value).matches()) {
                            mProperties.put((String)element, (String)value);
                        }
                        break;
                    case BOOLEAN:
                        index = (Integer)value;
                        mProperties.put((String)element, HardwareProperties.BOOLEAN_VALUES[index]);
                        break;
                    case STRING_ENUM:
                    case INTEGER_ENUM:
                        // For a combo, value is the index of the enum to use.
                        index = (Integer)value;
                        String[] values = property.getEnum();
                        if (values != null && values.length > index) {
                            mProperties.put((String)element, values[index]);
                        }
                        break;
                }
                mHardwareViewer.refresh(element);
            }

            @Override
            protected Object getValue(Object element) {
                String hardwareName = (String)element;
                HardwareProperty property = mHardwareMap.get(hardwareName);
                String value = mProperties.get(hardwareName);
                switch (property.getType()) {
                    case INTEGER:
                        // intended fall-through.
                    case DISKSIZE:
                        return value;
                    case BOOLEAN:
                        return HardwareProperties.getBooleanValueIndex(value);
                    case STRING_ENUM:
                    case INTEGER_ENUM:
                        // For a combo, we need to return the index of the value in the enum
                        String[] values = property.getEnum();
                        if (values != null) {
                            for (int i = 0; i < values.length; i++) {
                                if (values[i].equals(value)) {
                                    return i;
                                }
                            }
                        }
                }

                return null;
            }

            @Override
            protected CellEditor getCellEditor(Object element) {
                String hardwareName = (String)element;
                HardwareProperty property = mHardwareMap.get(hardwareName);
                switch (property.getType()) {
                    // TODO: custom TextCellEditor that restrict input.
                    case INTEGER:
                        // intended fall-through.
                    case DISKSIZE:
                        return new TextCellEditor(hardwareTable);
                    case BOOLEAN:
                        return new ComboBoxCellEditor(hardwareTable,
                                HardwareProperties.BOOLEAN_VALUES,
                                SWT.READ_ONLY | SWT.DROP_DOWN);
                    case STRING_ENUM:
                    case INTEGER_ENUM:
                        String[] values = property.getEnum();
                        if (values != null && values.length > 0) {
                            return new ComboBoxCellEditor(hardwareTable,
                                    values,
                                    SWT.READ_ONLY | SWT.DROP_DOWN);
                        }
                }
                return null;
            }

            @Override
            protected boolean canEdit(Object element) {
                String hardwareName = (String)element;
                HardwareProperty property = mHardwareMap.get(hardwareName);
                return property != null;
            }
        });


        mHardwareViewer.setInput(mProperties);
    }

    // -- Start of internal part ----------
    // Hide everything down-below from SWT designer
    //$hide>>$

    /**
     * When editing an existing AVD info, fill the UI that has just been created with
     * the values from the AVD.
     */
    public void fillExistingAvdInfo() {
        if (mEditAvdInfo == null) {
            return;
        }

        mAvdName.setText(mEditAvdInfo.getName());

        Map<String, String> props = mEditAvdInfo.getProperties();

        IAndroidTarget target = mEditAvdInfo.getTarget();
        if (target != null && !mCurrentTargets.isEmpty()) {
            // Try to select the target in the target combo.
            // This will fail if the AVD needs to be repaired.
            //
            // This is a linear search but the list is always
            // small enough and we only do this once.
            int n = mTargetCombo.getItemCount();
            for (int i = 0;i < n; i++) {
                if (target.equals(mCurrentTargets.get(mTargetCombo.getItem(i)))) {
                    mTargetCombo.select(i);
                    reloadAbiTypeCombo();
                    reloadSkinCombo();
                    break;
                }
            }
        }

        // select the abi type
        ISystemImage[] systemImages = getSystemImages(target);
        if (target != null && systemImages.length > 0) {
            mAbiTypeCombo.setEnabled(systemImages.length > 1);
            String abiType = AvdInfo.getPrettyAbiType(mEditAvdInfo.getAbiType());
            int n = mAbiTypeCombo.getItemCount();
            for (int i = 0; i < n; i++) {
                if (abiType.equals(mAbiTypeCombo.getItem(i))) {
                    mAbiTypeCombo.select(i);
                    reloadSkinCombo();
                    break;
                }
            }
        }

        if (props != null) {

            // First try the skin name and if it doesn't work fallback on the skin path
            nextSkin: for (int s = 0; s < 2; s++) {
                String skin = props.get(s == 0 ? AvdManager.AVD_INI_SKIN_NAME
                                               : AvdManager.AVD_INI_SKIN_PATH);
                if (skin != null && skin.length() > 0) {
                    Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin);
                    if (m.matches() && m.groupCount() == 2) {
                        enableSkinWidgets(false);
                        mSkinListRadio.setSelection(false);
                        mSkinSizeRadio.setSelection(true);
                        mSkinSizeWidth.setText(m.group(1));
                        mSkinSizeHeight.setText(m.group(2));
                        break nextSkin;
                    } else {
                        enableSkinWidgets(true);
                        mSkinSizeRadio.setSelection(false);
                        mSkinListRadio.setSelection(true);

                        int n = mSkinCombo.getItemCount();
                        for (int i = 0; i < n; i++) {
                            if (skin.equals(mSkinCombo.getItem(i))) {
                                mSkinCombo.select(i);
                                break nextSkin;
                            }
                        }
                    }
                }
            }

            String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH);
            if (sdcard != null && sdcard.length() > 0) {
                enableSdCardWidgets(false);
                mSdCardSizeRadio.setSelection(false);
                mSdCardFileRadio.setSelection(true);
                mSdCardFile.setText(sdcard);
            }

            sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE);
            if (sdcard != null && sdcard.length() > 0) {
                String[] values = new String[2];
                long sdcardSize = AvdManager.parseSdcardSize(sdcard, values);

                if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) {
                    enableSdCardWidgets(true);
                    mSdCardFileRadio.setSelection(false);
                    mSdCardSizeRadio.setSelection(true);

                    mSdCardSize.setText(values[0]);

                    String suffix = values[1];
                    int n = mSdCardSizeCombo.getItemCount();
                    for (int i = 0; i < n; i++) {
                        if (mSdCardSizeCombo.getItem(i).startsWith(suffix)) {
                            mSdCardSizeCombo.select(i);
                        }
                    }
                }
            }

            String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
            if (snapshots != null && snapshots.length() > 0) {
                mSnapshotCheck.setSelection(snapshots.equals("true"));
            }
        }

        mProperties.clear();

        if (props != null) {
            for (Entry<String, String> entry : props.entrySet()) {
                HardwareProperty prop = mHardwareMap.get(entry.getKey());
                if (prop != null && prop.isValidForUi()) {
                    mProperties.put(entry.getKey(), entry.getValue());
                }
            }
        }

        // Cleanup known non-hardware properties
        mProperties.remove(AvdManager.AVD_INI_ABI_TYPE);
        mProperties.remove(AvdManager.AVD_INI_CPU_ARCH);
        mProperties.remove(AvdManager.AVD_INI_SKIN_PATH);
        mProperties.remove(AvdManager.AVD_INI_SKIN_NAME);
        mProperties.remove(AvdManager.AVD_INI_SDCARD_SIZE);
        mProperties.remove(AvdManager.AVD_INI_SDCARD_PATH);
        mProperties.remove(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
        mProperties.remove(AvdManager.AVD_INI_IMAGES_1);
        mProperties.remove(AvdManager.AVD_INI_IMAGES_2);

        mHardwareViewer.refresh();
    }

    @Override
    protected void okPressed() {
        if (createAvd()) {
            super.okPressed();
        }
    }

    /**
     * Enable or disable the sd card widgets.
     * @param sizeMode if true the size-based widgets are to be enabled, and the file-based ones
     * disabled.
     */
    private void enableSdCardWidgets(boolean sizeMode) {
        mSdCardSize.setEnabled(sizeMode);
        mSdCardSizeCombo.setEnabled(sizeMode);

        mSdCardFile.setEnabled(!sizeMode);
        mBrowseSdCard.setEnabled(!sizeMode);
    }

    /**
     * Enable or disable the skin widgets.
     * @param listMode if true the list-based widgets are to be enabled, and the size-based ones
     * disabled.
     */
    private void enableSkinWidgets(boolean listMode) {
        mSkinCombo.setEnabled(listMode);

        mSkinSizeWidth.setEnabled(!listMode);
        mSkinSizeHeight.setEnabled(!listMode);
    }


    private void onBrowseSdCard() {
        FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN);
        dlg.setText("Choose SD Card image file.");

        String fileName = dlg.open();
        if (fileName != null) {
            mSdCardFile.setText(fileName);
        }
    }



    private void reloadTargetCombo() {
        String selected = null;
        int index = mTargetCombo.getSelectionIndex();
        if (index >= 0) {
            selected = mTargetCombo.getItem(index);
        }

        mCurrentTargets.clear();
        mTargetCombo.removeAll();

        boolean found = false;
        index = -1;

        SdkManager sdkManager = mAvdManager.getSdkManager();
        if (sdkManager != null) {
            for (IAndroidTarget target : sdkManager.getTargets()) {
                String name;
                if (target.isPlatform()) {
                    name = String.format("%s - API Level %s",
                            target.getName(),
                            target.getVersion().getApiString());
                } else {
                    name = String.format("%s (%s) - API Level %s",
                            target.getName(),
                            target.getVendor(),
                            target.getVersion().getApiString());
                }
                mCurrentTargets.put(name, target);
                mTargetCombo.add(name);
                if (!found) {
                    index++;
                    found = name.equals(selected);
                }
            }
        }

        mTargetCombo.setEnabled(mCurrentTargets.size() > 0);

        if (found) {
            mTargetCombo.select(index);
        }

        reloadSkinCombo();
    }

    private void reloadSkinCombo() {
        String selected = null;
        int index = mSkinCombo.getSelectionIndex();
        if (index >= 0) {
            selected = mSkinCombo.getItem(index);
        }

        mSkinCombo.removeAll();
        mSkinCombo.setEnabled(false);

        index = mTargetCombo.getSelectionIndex();
        if (index >= 0) {

            String targetName = mTargetCombo.getItem(index);

            boolean found = false;
            IAndroidTarget target = mCurrentTargets.get(targetName);
            if (target != null) {
                mSkinCombo.add(String.format("Default (%s)", target.getDefaultSkin()));

                index = -1;
                for (String skin : target.getSkins()) {
                    mSkinCombo.add(skin);
                    if (!found) {
                        index++;
                        found = skin.equals(selected);
                    }
                }

                mSkinCombo.setEnabled(true);

                if (found) {
                    mSkinCombo.select(index);
                } else {
                    mSkinCombo.select(0);  // default
                    loadSkin();
                }
            }
        }
    }

    /**
    * Reload all the abi types in the selection list
    */
    private void reloadAbiTypeCombo() {
       String selected = null;
       boolean found = false;

       int index = mTargetCombo.getSelectionIndex();
       if (index >= 0) {
           String targetName = mTargetCombo.getItem(index);
           IAndroidTarget target = mCurrentTargets.get(targetName);

           ISystemImage[] systemImages = getSystemImages(target);

           mAbiTypeCombo.setEnabled(systemImages.length > 1);

           // If user explicitly selected an ABI before, preserve that option
           // If user did not explicitly select before (only one option before)
           // force them to select
           index = mAbiTypeCombo.getSelectionIndex();
           if (index >= 0 && mAbiTypeCombo.getItemCount() > 1) {
               selected = mAbiTypeCombo.getItem(index);
           }

           mAbiTypeCombo.removeAll();

           int i;
           for ( i = 0; i < systemImages.length ; i++ ) {
               String prettyAbiType = AvdInfo.getPrettyAbiType(systemImages[i].getAbiType());
               mAbiTypeCombo.add(prettyAbiType);
               if (!found) {
                   found = prettyAbiType.equals(selected);
                   if (found) {
                       mAbiTypeCombo.select(i);
                   }
               }
           }

           if (systemImages.length == 1) {
               mAbiTypeCombo.select(0);
           }
       }
    }

    /**
     * Validates the fields, displays errors and warnings.
     * Enables the finish button if there are no errors.
     */
    private void validatePage() {
        String error = null;
        String warning = null;

        // Validate AVD name
        String avdName = mAvdName.getText().trim();
        boolean hasAvdName = avdName.length() > 0;
        boolean isCreate = mEditAvdInfo == null || !avdName.equals(mEditAvdInfo.getName());

        if (hasAvdName && !AvdManager.RE_AVD_NAME.matcher(avdName).matches()) {
            error = String.format(
                "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s",
                avdName, AvdManager.CHARS_AVD_NAME);
        }

        // Validate target
        if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() < 0) {
            error = "A target must be selected in order to create an AVD.";
        }

        // validate abi type if the selected target supports multi archs.
        if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() > 0) {
            int index = mTargetCombo.getSelectionIndex();
            String targetName = mTargetCombo.getItem(index);
            IAndroidTarget target = mCurrentTargets.get(targetName);
            ISystemImage[] systemImages = getSystemImages(target);
            if (systemImages.length > 1 && mAbiTypeCombo.getSelectionIndex() < 0) {
               error = "An ABI type must be selected in order to create an AVD.";
            }
        }

        // Validate SDCard path or value
        if (error == null) {
            // get the mode. We only need to check the file since the
            // verifier on the size Text will prevent invalid input
            boolean sdcardFileMode = mSdCardFileRadio.getSelection();
            if (sdcardFileMode) {
                String sdName = mSdCardFile.getText().trim();
                if (sdName.length() > 0 && !new File(sdName).isFile()) {
                    error = "SD Card path isn't valid.";
                }
            } else {
                String valueString = mSdCardSize.getText();
                if (valueString.length() > 0) {
                    long value = 0;
                    try {
                        value = Long.parseLong(valueString);

                        int sizeIndex = mSdCardSizeCombo.getSelectionIndex();
                        if (sizeIndex >= 0) {
                            // index 0 shifts by 10 (1024=K), index 1 by 20, etc.
                            value <<= 10*(1 + sizeIndex);
                        }

                        if (value < AvdManager.SDCARD_MIN_BYTE_SIZE ||
                                value > AvdManager.SDCARD_MAX_BYTE_SIZE) {
                            value = 0;
                        }
                    } catch (Exception e) {
                        // ignore, we'll test value below.
                    }
                    if (value <= 0) {
                        error = "SD Card size is invalid. Range is 9 MiB..1023 GiB.";
                    } else if (mEditAvdInfo != null) {
                        // When editing an existing AVD, compare with the existing
                        // sdcard size, if any. It only matters if there was an sdcard setting
                        // before.
                        Map<String, String> props = mEditAvdInfo.getProperties();
                        if (props != null) {
                            String original =
                                mEditAvdInfo.getProperties().get(AvdManager.AVD_INI_SDCARD_SIZE);
                            if (original != null && original.length() > 0) {
                                long originalSize =
                                    AvdManager.parseSdcardSize(original, null/*parsedStrings*/);
                                if (originalSize > 0 && value != originalSize) {
                                    warning = "A new SD Card file will be created.\nThe current SD Card file will be lost.";
                                }
                            }
                        }
                    }
                }
            }
        }

        // validate the skin
        if (error == null) {
            // get the mode, we should only check if it's in size mode since
            // the built-in list mode is always valid.
            if (mSkinSizeRadio.getSelection()) {
                // need both with and heigh to be non null
                String width = mSkinSizeWidth.getText();   // no need for trim, since the verifier
                String height = mSkinSizeHeight.getText(); // rejects non digit.

                if (width.length() == 0 || height.length() == 0) {
                    error = "Skin size is incorrect.\nBoth dimensions must be > 0.";
                }
            }
        }

        // Check for duplicate AVD name
        if (isCreate && hasAvdName && error == null && !mForceCreation.getSelection()) {
            Pair<AvdConflict, String> conflict = mAvdManager.isAvdNameConflicting(avdName);
            assert conflict != null;
            switch(conflict.getFirst()) {
            case NO_CONFLICT:
                break;
            case CONFLICT_EXISTING_AVD:
            case CONFLICT_INVALID_AVD:
                error = String.format(
                        "The AVD name '%s' is already used.\n" +
                        "Check \"Override the existing AVD\" to delete the existing one.",
                        avdName);
                break;
            case CONFLICT_EXISTING_PATH:
                error = String.format(
                        "Conflict with %s\n" +
                        "Check \"Override the existing AVD\" to delete the existing one.",
                        conflict.getSecond());
                break;
            default:
                // Hmm not supposed to happen... probably someone expanded the
                // enum without adding something here. In this case just do an
                // assert and use a generic error message.
                error = String.format(
                        "Conflict %s with %s.\n" +
                        "Check \"Override the existing AVD\" to delete the existing one.",
                        conflict.getFirst().toString(),
                        conflict.getSecond());
                assert false;
                break;
            }
        }

        if (error == null && mEditAvdInfo != null && isCreate) {
            warning = String.format("The AVD '%1$s' will be duplicated into '%2$s'.",
                    mEditAvdInfo.getName(),
                    avdName);
        }

        // Validate the create button
        boolean can_create = hasAvdName && error == null;
        if (can_create) {
            can_create &= mTargetCombo.getSelectionIndex() >= 0;
        }
        mOkButton.setEnabled(can_create);

        // Adjust the create button label as needed
        if (isCreate) {
            mOkButton.setText("Create AVD");
        } else {
            mOkButton.setText("Edit AVD");
        }

        // -- update UI
        if (error != null) {
            mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png"));  //$NON-NLS-1$
            mStatusLabel.setText(error);
        } else if (warning != null) {
            mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png"));  //$NON-NLS-1$
            mStatusLabel.setText(warning);
        } else {
            mStatusIcon.setImage(null);
            mStatusLabel.setText(" \n "); //$NON-NLS-1$
        }

        mStatusComposite.pack(true);
    }

    private void loadSkin() {
        int targetIndex = mTargetCombo.getSelectionIndex();
        if (targetIndex < 0) {
            return;
        }

        // resolve the target.
        String targetName = mTargetCombo.getItem(targetIndex);
        IAndroidTarget target = mCurrentTargets.get(targetName);
        if (target == null) {
            return;
        }

        // get the skin name
        String skinName = null;
        int skinIndex = mSkinCombo.getSelectionIndex();
        if (skinIndex < 0) {
            return;
        } else if (skinIndex == 0) { // default skin for the target
            skinName = target.getDefaultSkin();
        } else {
            skinName = mSkinCombo.getItem(skinIndex);
        }

        // load the skin properties
        String path = target.getPath(IAndroidTarget.SKINS);
        File skin = new File(path, skinName);
        if (skin.isDirectory() == false && target.isPlatform() == false) {
            // it's possible the skin is in the parent target
            path = target.getParent().getPath(IAndroidTarget.SKINS);
            skin = new File(path, skinName);
        }

        if (skin.isDirectory() == false) {
            return;
        }

        // now get the hardware.ini from the add-on (if applicable) and from the skin
        // (if applicable)
        HashMap<String, String> hardwareValues = new HashMap<String, String>();
        if (target.isPlatform() == false) {
            FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(),
                    AvdManager.HARDWARE_INI);
            if (targetHardwareFile.isFile()) {
                Map<String, String> targetHardwareConfig = ProjectProperties.parsePropertyFile(
                        targetHardwareFile, null /*log*/);

                if (targetHardwareConfig != null) {
                    hardwareValues.putAll(targetHardwareConfig);
                }
            }
        }

        // from the skin
        FileWrapper skinHardwareFile = new FileWrapper(skin, AvdManager.HARDWARE_INI);
        if (skinHardwareFile.isFile()) {
            Map<String, String> skinHardwareConfig = ProjectProperties.parsePropertyFile(
                    skinHardwareFile, null /*log*/);

            if (skinHardwareConfig != null) {
                hardwareValues.putAll(skinHardwareConfig);
            }
        }

        // now set those values in the list of properties for the AVD.
        // We just check that none of those properties have been edited by the user yet.
        for (Entry<String, String> entry : hardwareValues.entrySet()) {
            if (mEditedProperties.contains(entry.getKey()) == false) {
                mProperties.put(entry.getKey(), entry.getValue());
            }
        }

        mHardwareViewer.refresh();
    }

    /**
     * Creates an AVD from the values in the UI. Called when the user presses the OK button.
     */
    private boolean createAvd() {
        String avdName = mAvdName.getText().trim();
        int index = mTargetCombo.getSelectionIndex();

        // quick check on the name and the target selection
        if (avdName.length() == 0 || index < 0) {
            return false;
        }

        // resolve the target.
        String targetName = mTargetCombo.getItem(index);
        IAndroidTarget target = mCurrentTargets.get(targetName);
        if (target == null) {
            return false;
        }

        // get the abi type
        mAbiType = SdkConstants.ABI_ARMEABI;
        ISystemImage[] systemImages = getSystemImages(target);
        if (systemImages.length > 0) {
            int abiIndex = mAbiTypeCombo.getSelectionIndex();
            if (abiIndex >= 0) {
                String prettyname = mAbiTypeCombo.getItem(abiIndex);
                //Extract the abi type
                int firstIndex = prettyname.indexOf("(");
                int lastIndex = prettyname.indexOf(")");
                mAbiType = prettyname.substring(firstIndex+1, lastIndex);
            }
        }

        // get the SD card data from the UI.
        String sdName = null;
        if (mSdCardSizeRadio.getSelection()) {
            // size mode
            String value = mSdCardSize.getText().trim();
            if (value.length() > 0) {
                sdName = value;
                // add the unit
                switch (mSdCardSizeCombo.getSelectionIndex()) {
                    case 0:
                        sdName += "K";  //$NON-NLS-1$
                        break;
                    case 1:
                        sdName += "M";  //$NON-NLS-1$
                        break;
                    case 2:
                        sdName += "G";  //$NON-NLS-1$
                        break;
                    default:
                        // shouldn't be here
                        assert false;
                }
            }
        } else {
            // file mode.
            sdName = mSdCardFile.getText().trim();
        }

        // get the Skin data from the UI
        String skinName = null;
        if (mSkinListRadio.getSelection()) {
            // built-in list provides the skin
            int skinIndex = mSkinCombo.getSelectionIndex();
            if (skinIndex > 0) {
                // index 0 is the default, we don't use it
                skinName = mSkinCombo.getItem(skinIndex);
            }
        } else {
            // size mode. get both size and writes it as a skin name
            // thanks to validatePage() we know the content of the fields is correct
            skinName = mSkinSizeWidth.getText() + "x" + mSkinSizeHeight.getText(); //$NON-NLS-1$
        }

        ILogger log = mSdkLog;
        if (log == null || log instanceof MessageBoxLog) {
            // If the current logger is a message box, we use our own (to make sure
            // to display errors right away and customize the title).
            log = new MessageBoxLog(
                    String.format("Result of creating AVD '%s':", avdName),
                    getContents().getDisplay(),
                    false /*logErrorsOnly*/);
        }

        File avdFolder = null;
        try {
            avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName);
        } catch (AndroidLocationException e) {
            return false;
        }

        boolean force = mForceCreation.getSelection();
        boolean snapshot = mSnapshotCheck.getSelection();

        boolean success = false;
        AvdInfo avdInfo = mAvdManager.createAvd(
                avdFolder,
                avdName,
                target,
                mAbiType,
                skinName,
                sdName,
                mProperties,
                snapshot,
                force,
                mEditAvdInfo != null, //edit existing
                log);

        success = avdInfo != null;

        if (log instanceof MessageBoxLog) {
            ((MessageBoxLog) log).displayResult(success);
        }
        return success;
    }

    /**
     * Returns the list of system images of a target.
     * <p/>
     * If target is null, returns an empty list.
     * If target is an add-on with no system images, return the list from its parent platform.
     *
     * @param target An IAndroidTarget. Can be null.
     * @return A non-null ISystemImage array. Can be empty.
     */
    private ISystemImage[] getSystemImages(IAndroidTarget target) {
        if (target != null) {
            ISystemImage[] images = target.getSystemImages();

            if ((images == null || images.length == 0) && !target.isPlatform()) {
                // If an add-on does not provide any system images, use the ones from the parent.
                images = target.getParent().getSystemImages();
            }

            if (images != null) {
                return images;
            }
        }

        return new ISystemImage[0];
    }

    // End of hiding from SWT Designer
    //$hide<<$
}
