/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2017 Kicad Developers, see change_log.txt for contributors.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include "microwave_tool.h"

#include <gal/graphics_abstraction_layer.h>
#include <class_draw_panel_gal.h>
#include <view/view_controls.h>
#include <view/view.h>
#include <tool/tool_manager.h>
#include <board_commit.h>
#include <confirm.h>
#include <preview_items/two_point_geom_manager.h>
#include <preview_items/centreline_rect_item.h>

// For frame ToolID values
#include <pcbnew_id.h>

// For action icons
#include <bitmaps.h>

#include <class_board_item.h>
#include <class_module.h>

#include <microwave/microwave_inductor.h>

#include "pcb_actions.h"
#include "selection_tool.h"
#include "tool_event_utils.h"


///> Type of items that are "simple" - just get placed on
///> the board directly, without a graphical interactive setup stage
enum MWAVE_TOOL_SIMPLE_ID
{
    GAP,
    STUB,
    STUB_ARC,
    FUNCTION_SHAPE,
};

TOOL_ACTION PCB_ACTIONS::microwaveCreateGap(
        "pcbnew.MicrowaveTool.createGap",
        AS_GLOBAL, 0,
        _( "Add Gap" ), _( "Create gap of specified length for microwave applications" ),
        mw_add_gap_xpm, AF_ACTIVATE, (void*) MWAVE_TOOL_SIMPLE_ID::GAP );

TOOL_ACTION PCB_ACTIONS::microwaveCreateStub(
        "pcbnew.MicrowaveTool.createStub",
        AS_GLOBAL, 0,
        _( "Add Stub" ), _( "Create stub of specified length for microwave applications" ),
        mw_add_stub_xpm, AF_ACTIVATE, (void*) MWAVE_TOOL_SIMPLE_ID::STUB );

TOOL_ACTION PCB_ACTIONS::microwaveCreateStubArc(
        "pcbnew.MicrowaveTool.createStubArc",
        AS_GLOBAL, 0,
        _( "Add Arc Stub" ), _( "Create stub (arc) of specified length for microwave applications" ),
        mw_add_stub_arc_xpm, AF_ACTIVATE, (void*) MWAVE_TOOL_SIMPLE_ID::STUB_ARC );

TOOL_ACTION PCB_ACTIONS::microwaveCreateFunctionShape(
        "pcbnew.MicrowaveTool.createFunctionShape",
        AS_GLOBAL, 0,
        _( "Add Polynomial Shape" ), _( "Create polynomial shape for microwave applications" ),
        mw_add_gap_xpm, AF_ACTIVATE, (void*) MWAVE_TOOL_SIMPLE_ID::FUNCTION_SHAPE );

TOOL_ACTION PCB_ACTIONS::microwaveCreateLine(
        "pcbnew.MicrowaveTool.createLine",
        AS_GLOBAL, 0,
        _( "Add Microwave Line" ), _( "Create line of specified length for microwave applications" ),
        mw_add_line_xpm, AF_ACTIVATE );


MICROWAVE_TOOL::MICROWAVE_TOOL() :
        PCB_TOOL( "pcbnew.MicrowaveTool" ),
        m_menu( *this )
{
}


MICROWAVE_TOOL::~MICROWAVE_TOOL()
{}


void MICROWAVE_TOOL::Reset( RESET_REASON aReason )
{
}


bool MICROWAVE_TOOL::Init()
{
    auto activeToolFunctor = [] ( const SELECTION& aSel ) {
        return true;
    };

    auto& ctxMenu = m_menu.GetMenu();

    // cancel current goes in main context menu at the top if present
    ctxMenu.AddItem( ACTIONS::cancelInteractive, activeToolFunctor, 1000 );
    ctxMenu.AddSeparator( activeToolFunctor, 1000 );

    m_menu.AddStandardSubMenus( *getEditFrame<PCB_BASE_FRAME>() );

    return true;
}


struct MICROWAVE_TOOL_INFO
{
    using MOD_CREATOR = std::function<std::unique_ptr<MODULE>()>;

    wxString  name;
    int       toolId;
    MOD_CREATOR creatorFunc;
};


MICROWAVE_TOOL_INFO getMicrowaveItemCreator( PCB_EDIT_FRAME& frame, int aParam )
{
    MICROWAVE_TOOL_INFO info;

    switch( aParam )
    {
    case MWAVE_TOOL_SIMPLE_ID::GAP:
        info.name =  _( "Add Gap" );
        info.toolId = ID_PCB_MUWAVE_TOOL_GAP_CMD;
        info.creatorFunc = [&frame] () {
            return std::unique_ptr<MODULE>( frame.Create_MuWaveComponent( 0 ) );
        };
        break;

    case MWAVE_TOOL_SIMPLE_ID::STUB:
        info.name =  _( "Add Stub" );
        info.toolId = ID_PCB_MUWAVE_TOOL_STUB_CMD;
        info.creatorFunc = [&frame] () {
            return std::unique_ptr<MODULE>( frame.Create_MuWaveComponent( 1 ) );
        };
        break;

    case MWAVE_TOOL_SIMPLE_ID::STUB_ARC:
        info.name =  _( "Add Stub (Arc)" );
        info.toolId = ID_PCB_MUWAVE_TOOL_STUB_ARC_CMD;
        info.creatorFunc = [&frame] () {
            return std::unique_ptr<MODULE>( frame.Create_MuWaveComponent( 2 ) );
        };
        break;

    case MWAVE_TOOL_SIMPLE_ID::FUNCTION_SHAPE:
        info.name =  _( "Add Polynomial Shape" );
        info.toolId = ID_PCB_MUWAVE_TOOL_FUNCTION_SHAPE_CMD;
        info.creatorFunc = [&frame] () {
            return std::unique_ptr<MODULE>( frame.Create_MuWavePolygonShape() );
        };
        break;

    default:
        // Avoid uninitilized value:
        info.toolId = 0;
        // info.name is already set to empty string
        break;
    };

    return info;
}


int MICROWAVE_TOOL::addMicrowaveFootprint( const TOOL_EVENT& aEvent )
{
    auto& frame = *getEditFrame<PCB_EDIT_FRAME>();

    const int param = aEvent.Parameter<intptr_t>();

    MICROWAVE_TOOL_INFO info = getMicrowaveItemCreator( frame, param );

    // failed to find suitable item info - shouldn't be possible
    // if all the id's are handled
    if( !info.name )
    {
        wxASSERT_MSG( 0, "Failed to find suitable microwave tool info" );
        return 0;
    }

    frame.SetToolID( info.toolId, wxCURSOR_PENCIL, info.name );

    struct MICROWAVE_PLACER : public INTERACTIVE_PLACER_BASE
    {
        MICROWAVE_TOOL_INFO& m_info;

        MICROWAVE_PLACER( MICROWAVE_TOOL_INFO& aInfo ) :
            m_info( aInfo ) {};

        std::unique_ptr<BOARD_ITEM> CreateItem() override
        {
            auto module = m_info.creatorFunc();

            // Module has been added in the legacy backend,
            // so we have to remove it before committing the change
            // @todo LEGACY
            if( module )
            {
                m_board->Remove( module.get() );
            }

            return std::unique_ptr<BOARD_ITEM>( module.release() );
        }
    };

    MICROWAVE_PLACER placer ( info );

    doInteractiveItemPlacement( &placer,  _( "Place microwave feature" ) );

    frame.SetNoToolSelected();

    return 0;
}


void MICROWAVE_TOOL::createInductorBetween( const VECTOR2I& aStart, const VECTOR2I& aEnd )
{
    auto& frame = *getEditFrame<PCB_EDIT_FRAME>();

    MWAVE::INDUCTOR_PATTERN pattern;

    pattern.m_Width = board()->GetDesignSettings().GetCurrentTrackWidth();

    pattern.m_Start = { aStart.x, aStart.y };
    pattern.m_End = { aEnd.x, aEnd.y };

    wxString errorMessage;

    auto inductorModule = std::unique_ptr<MODULE>(
            CreateMicrowaveInductor( pattern, &frame, errorMessage )
    );

    if( inductorModule )
    {
        // legacy mode tools add to the board
        // so remove it and add it back with the commit object
        // this has to happen, even if we don't end up storing the module
        // @todo LEGACY
        board()->Remove( inductorModule.get() );
    }

    // on any error, report if we can
    if ( !inductorModule || !errorMessage.IsEmpty() )
    {
        if ( !errorMessage.IsEmpty() )
        {
            DisplayError( &frame, errorMessage );
        }
    }
    else
    {
        // at this point, we can save the module
        frame.SetCurItem( inductorModule.get() );

        BOARD_COMMIT commit( this );
        commit.Add( inductorModule.release() );
        commit.Push( _("Add microwave inductor" ) );
    }
}


static const COLOR4D inductorAreaFill( 0.3, 0.3, 0.5, 0.3 );
static const COLOR4D inductorAreaStroke( 0.4, 1.0, 1.0, 1.0 );
static const double  inductorAreaStrokeWidth = 1.0;

///> Aspect of the preview rectangle - this is hardcoded in the
///> microwave backend for now
static const double  inductorAreaAspect = 0.5;


int MICROWAVE_TOOL::drawMicrowaveInductor( const TOOL_EVENT& aEvent )
{
    using namespace KIGFX::PREVIEW;

    KIGFX::VIEW& view = *getView();
    KIGFX::VIEW_CONTROLS& controls = *getViewControls();
    auto& frame = *getEditFrame<PCB_EDIT_FRAME>();

    frame.SetToolID( ID_PCB_MUWAVE_TOOL_SELF_CMD, wxCURSOR_PENCIL, _( "Add Microwave Inductor" ) );

    Activate();

    TWO_POINT_GEOMETRY_MANAGER tpGeomMgr;

    CENTRELINE_RECT_ITEM previewRect( tpGeomMgr, inductorAreaAspect );

    previewRect.SetFillColor( inductorAreaFill );
    previewRect.SetStrokeColor( inductorAreaStroke );
    previewRect.SetLineWidth( inductorAreaStrokeWidth );

    bool originSet = false;

    controls.ShowCursor( true );
    controls.SetSnapping( true );

    view.Add( &previewRect );

    while( auto evt = Wait() )
    {
        VECTOR2I cursorPos = controls.GetCursorPosition();

        if( TOOL_EVT_UTILS::IsCancelInteractive( *evt ) )
        {
            // overriding action, or we're cancelling without
            // an in-progress preview area
            if ( evt->IsActivate() || !originSet )
            {
                break;
            }

            // had an in-progress area, so start again but don't
            // cancel the tool
            originSet = false;
            view.SetVisible( &previewRect, false );
            view.Update( &previewRect, KIGFX::GEOMETRY );
        }

        // A click or drag starts
        else if( !originSet &&
                ( evt->IsClick( BUT_LEFT ) || evt->IsDrag( BUT_LEFT ) ) )
        {
            tpGeomMgr.SetOrigin( cursorPos );
            tpGeomMgr.SetEnd( cursorPos );

            originSet = true;

            controls.CaptureCursor( true );
            controls.SetAutoPan( true );
        }

        // another click after origin set is the end
        // left up is also the end, as you'll only get that after a drag
        else if( originSet &&
                ( evt->IsClick( BUT_LEFT ) || evt->IsMouseUp( BUT_LEFT ) ) )
        {
            // second click, we're done:
            // delegate to the point-to-point inductor creator function
            createInductorBetween( tpGeomMgr.GetOrigin(), tpGeomMgr.GetEnd() );

            // start again if needed
            originSet = false;

            view.SetVisible( &previewRect, false );
            view.Update( &previewRect, KIGFX::GEOMETRY );
        }

        // any move or drag once the origin was set updates
        // the end point
        else if( originSet &&
                ( evt->IsMotion() || evt->IsDrag( BUT_LEFT ) ) )
        {
            tpGeomMgr.SetAngleSnap( evt->Modifier( MD_CTRL ) );
            tpGeomMgr.SetEnd( cursorPos );

            view.SetVisible( &previewRect, true );
            view.Update( &previewRect, KIGFX::GEOMETRY );
        }

        else if( evt->IsClick( BUT_RIGHT ) )
        {
            m_menu.ShowContextMenu();
        }
    }

    view.Remove( &previewRect );

    frame.SetNoToolSelected();

    return 0;
}


void MICROWAVE_TOOL::setTransitions()
{
    Go( &MICROWAVE_TOOL::addMicrowaveFootprint, PCB_ACTIONS::microwaveCreateGap.MakeEvent() );
    Go( &MICROWAVE_TOOL::addMicrowaveFootprint, PCB_ACTIONS::microwaveCreateStub.MakeEvent() );
    Go( &MICROWAVE_TOOL::addMicrowaveFootprint, PCB_ACTIONS::microwaveCreateStubArc.MakeEvent() );
    Go( &MICROWAVE_TOOL::addMicrowaveFootprint, PCB_ACTIONS::microwaveCreateFunctionShape.MakeEvent() );

    Go( &MICROWAVE_TOOL::drawMicrowaveInductor, PCB_ACTIONS::microwaveCreateLine.MakeEvent() );
}
