/**CFile****************************************************************

  FileName    [lpkCut.c]

  SystemName  [ABC: Logic synthesis and verification system.]

  PackageName [Fast Boolean matching for LUT structures.]

  Synopsis    []

  Author      [Alan Mishchenko]
  
  Affiliation [UC Berkeley]

  Date        [Ver. 1.0. Started - April 28, 2007.]

  Revision    [$Id: lpkCut.c,v 1.00 2007/04/28 00:00:00 alanmi Exp $]

***********************************************************************/

#include "lpkInt.h"
#include "bool/kit/cloud.h"

ABC_NAMESPACE_IMPL_START


////////////////////////////////////////////////////////////////////////
///                        DECLARATIONS                              ///
////////////////////////////////////////////////////////////////////////
 
////////////////////////////////////////////////////////////////////////
///                     FUNCTION DEFINITIONS                         ///
////////////////////////////////////////////////////////////////////////

/**Function*************************************************************

  Synopsis    [Computes the truth table of one cut.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
CloudNode * Lpk_CutTruthBdd_rec( CloudManager * dd, Hop_Man_t * pMan, Hop_Obj_t * pObj, int nVars )
{
    CloudNode * pTruth, * pTruth0, * pTruth1;
    assert( !Hop_IsComplement(pObj) );
    if ( pObj->pData )
    {
        assert( ((unsigned)(ABC_PTRUINT_T)pObj->pData) & 0xffff0000 );
        return (CloudNode *)pObj->pData;
    }
    // get the plan for a new truth table
    if ( Hop_ObjIsConst1(pObj) )
        pTruth = dd->one;
    else
    {
        assert( Hop_ObjIsAnd(pObj) );
        // compute the truth tables of the fanins
        pTruth0 = Lpk_CutTruthBdd_rec( dd, pMan, Hop_ObjFanin0(pObj), nVars );
        pTruth1 = Lpk_CutTruthBdd_rec( dd, pMan, Hop_ObjFanin1(pObj), nVars );
        pTruth0 = Cloud_NotCond( pTruth0, Hop_ObjFaninC0(pObj) );
        pTruth1 = Cloud_NotCond( pTruth1, Hop_ObjFaninC1(pObj) );
        // creat the truth table of the node
        pTruth = Cloud_bddAnd( dd, pTruth0, pTruth1 );
    }
    pObj->pData = pTruth;
    return pTruth;
}

/**Function*************************************************************

  Synopsis    [Verifies that the factoring is correct.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
CloudNode * Lpk_CutTruthBdd( Lpk_Man_t * p, Lpk_Cut_t * pCut )
{
    CloudManager * dd = p->pDsdMan->dd;
    Hop_Man_t * pManHop = (Hop_Man_t *)p->pNtk->pManFunc;
    Hop_Obj_t * pObjHop;
    Abc_Obj_t * pObj, * pFanin;
    CloudNode * pTruth = NULL; // Suppress "might be used uninitialized"
    int i, k;

//    return NULL;
//    Lpk_NodePrintCut( p, pCut );
    // initialize the leaves
    Lpk_CutForEachLeaf( p->pNtk, pCut, pObj, i )
        pObj->pCopy = (Abc_Obj_t *)dd->vars[pCut->nLeaves-1-i];

    // construct truth table in the topological order
    Lpk_CutForEachNodeReverse( p->pNtk, pCut, pObj, i )
    {
        // get the local AIG
        pObjHop = Hop_Regular((Hop_Obj_t *)pObj->pData);
        // clean the data field of the nodes in the AIG subgraph
        Hop_ObjCleanData_rec( pObjHop );
        // set the initial truth tables at the fanins
        Abc_ObjForEachFanin( pObj, pFanin, k )
        {
            assert( ((unsigned)(ABC_PTRUINT_T)pFanin->pCopy) & 0xffff0000 );
            Hop_ManPi( pManHop, k )->pData = pFanin->pCopy;
        }
        // compute the truth table of internal nodes
        pTruth = Lpk_CutTruthBdd_rec( dd, pManHop, pObjHop, pCut->nLeaves );
        if ( Hop_IsComplement((Hop_Obj_t *)pObj->pData) )
            pTruth = Cloud_Not(pTruth);
        // set the truth table at the node
        pObj->pCopy = (Abc_Obj_t *)pTruth;
        
    }

//    Cloud_bddPrint( dd, pTruth );
//    printf( "Bdd size = %d. Total nodes = %d.\n", Cloud_DagSize( dd, pTruth ), dd->nNodesCur-dd->nVars-1 );
    return pTruth;
}


/**Function*************************************************************

  Synopsis    [Computes the truth table of one cut.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
unsigned * Lpk_CutTruth_rec( Hop_Man_t * pMan, Hop_Obj_t * pObj, int nVars, Vec_Ptr_t * vTtNodes, int * piCount )
{
    unsigned * pTruth, * pTruth0, * pTruth1;
    assert( !Hop_IsComplement(pObj) );
    if ( pObj->pData )
    {
        assert( ((unsigned)(ABC_PTRUINT_T)pObj->pData) & 0xffff0000 );
        return (unsigned *)pObj->pData;
    }
    // get the plan for a new truth table
    pTruth = (unsigned *)Vec_PtrEntry( vTtNodes, (*piCount)++ );
    if ( Hop_ObjIsConst1(pObj) )
        Kit_TruthFill( pTruth, nVars );
    else
    {
        assert( Hop_ObjIsAnd(pObj) );
        // compute the truth tables of the fanins
        pTruth0 = Lpk_CutTruth_rec( pMan, Hop_ObjFanin0(pObj), nVars, vTtNodes, piCount );
        pTruth1 = Lpk_CutTruth_rec( pMan, Hop_ObjFanin1(pObj), nVars, vTtNodes, piCount );
        // creat the truth table of the node
        Kit_TruthAndPhase( pTruth, pTruth0, pTruth1, nVars, Hop_ObjFaninC0(pObj), Hop_ObjFaninC1(pObj) );
    }
    pObj->pData = pTruth;
    return pTruth;
}

/**Function*************************************************************

  Synopsis    [Computes the truth able of one cut.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
unsigned * Lpk_CutTruth( Lpk_Man_t * p, Lpk_Cut_t * pCut, int fInv )
{
    Hop_Man_t * pManHop = (Hop_Man_t *)p->pNtk->pManFunc;
    Hop_Obj_t * pObjHop;
    Abc_Obj_t * pObj = NULL; // Suppress "might be used uninitialized"
    Abc_Obj_t * pFanin;
    unsigned * pTruth = NULL; // Suppress "might be used uninitialized"
    int i, k, iCount = 0;
//    Lpk_NodePrintCut( p, pCut );
    assert( pCut->nNodes > 0 );

    // initialize the leaves
    Lpk_CutForEachLeaf( p->pNtk, pCut, pObj, i )
        pObj->pCopy = (Abc_Obj_t *)Vec_PtrEntry( p->vTtElems, fInv? pCut->nLeaves-1-i : i );

    // construct truth table in the topological order
    Lpk_CutForEachNodeReverse( p->pNtk, pCut, pObj, i )
    {
        // get the local AIG
        pObjHop = Hop_Regular((Hop_Obj_t *)pObj->pData);
        // clean the data field of the nodes in the AIG subgraph
        Hop_ObjCleanData_rec( pObjHop );
        // set the initial truth tables at the fanins
        Abc_ObjForEachFanin( pObj, pFanin, k )
        {
            assert( ((unsigned)(ABC_PTRUINT_T)pFanin->pCopy) & 0xffff0000 );
            Hop_ManPi( pManHop, k )->pData = pFanin->pCopy;
        }
        // compute the truth table of internal nodes
        pTruth = Lpk_CutTruth_rec( pManHop, pObjHop, pCut->nLeaves, p->vTtNodes, &iCount );
        if ( Hop_IsComplement((Hop_Obj_t *)pObj->pData) )
            Kit_TruthNot( pTruth, pTruth, pCut->nLeaves );
        // set the truth table at the node
        pObj->pCopy = (Abc_Obj_t *)pTruth;
    }

    // make sure direct truth table is stored elsewhere (assuming the first call for direct truth!!!)
    if ( fInv == 0 )
    {
        pTruth = (unsigned *)Vec_PtrEntry( p->vTtNodes, iCount++ );
        Kit_TruthCopy( pTruth, (unsigned *)(ABC_PTRUINT_T)pObj->pCopy, pCut->nLeaves );
    }
    assert( iCount <= Vec_PtrSize(p->vTtNodes) );
    return pTruth;
}


/**Function*************************************************************

  Synopsis    [Returns 1 if at least one entry has changed.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
void Lpk_NodeRecordImpact( Lpk_Man_t * p )
{
    Lpk_Cut_t * pCut;
    Vec_Ptr_t * vNodes = Vec_VecEntry( p->vVisited, p->pObj->Id );
    Abc_Obj_t * pNode;
    int i, k;
    // collect the nodes that impact the given node
    Vec_PtrClear( vNodes );
    for ( i = 0; i < p->nCuts; i++ )
    {
        pCut = p->pCuts + i;
        for ( k = 0; k < (int)pCut->nLeaves; k++ )
        {
            pNode = Abc_NtkObj( p->pNtk, pCut->pLeaves[k] );
            if ( pNode->fMarkC )
                continue;
            pNode->fMarkC = 1;
            Vec_PtrPush( vNodes, (void *)(ABC_PTRUINT_T)pNode->Id );
            Vec_PtrPush( vNodes, (void *)(ABC_PTRUINT_T)Abc_ObjFanoutNum(pNode) );
        }
    }
    // clear the marks
    Vec_PtrForEachEntry( Abc_Obj_t *, vNodes, pNode, i )
    {
        pNode = Abc_NtkObj( p->pNtk, (int)(ABC_PTRUINT_T)pNode );
        pNode->fMarkC = 0;
        i++;
    }
//printf( "%d ", Vec_PtrSize(vNodes) );
}

/**Function*************************************************************

  Synopsis    [Returns 1 if the cut has structural DSD.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
int Lpk_NodeCutsCheckDsd( Lpk_Man_t * p, Lpk_Cut_t * pCut )
{
    Abc_Obj_t * pObj, * pFanin;
    int i, k, nCands, fLeavesOnly, RetValue;
    assert( pCut->nLeaves > 0 );
    // clear ref counters
    memset( p->pRefs, 0, sizeof(int) * pCut->nLeaves );
    // mark cut leaves
    Lpk_CutForEachLeaf( p->pNtk, pCut, pObj, i )
    {
        assert( pObj->fMarkA == 0 );
        pObj->fMarkA = 1;
        pObj->pCopy = (Abc_Obj_t *)(ABC_PTRUINT_T)i;
    }
    // ref leaves pointed from the internal nodes
    nCands = 0;
    Lpk_CutForEachNode( p->pNtk, pCut, pObj, i )
    {
        fLeavesOnly = 1;
        Abc_ObjForEachFanin( pObj, pFanin, k )
            if ( pFanin->fMarkA )
                p->pRefs[(int)(ABC_PTRUINT_T)pFanin->pCopy]++;
            else
                fLeavesOnly = 0;
        if ( fLeavesOnly )
            p->pCands[nCands++] = pObj->Id;
    }
    // look at the nodes that only point to the leaves
    RetValue = 0;
    for ( i = 0; i < nCands; i++ )
    {
        pObj = Abc_NtkObj( p->pNtk, p->pCands[i] );
        Abc_ObjForEachFanin( pObj, pFanin, k )
        {
            assert( pFanin->fMarkA == 1 );
            if ( p->pRefs[(int)(ABC_PTRUINT_T)pFanin->pCopy] > 1 )
                break;
        }
        if ( k == Abc_ObjFaninNum(pObj) )
        {
            RetValue = 1;
            break;
        }
    }
    // unmark cut leaves
    Lpk_CutForEachLeaf( p->pNtk, pCut, pObj, i )
        pObj->fMarkA = 0;
    return RetValue;
}

/**Function*************************************************************

  Synopsis    [Returns 1 if pDom is contained in pCut.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
static inline int Lpk_NodeCutsOneDominance( Lpk_Cut_t * pDom, Lpk_Cut_t * pCut )
{
    int i, k;
    for ( i = 0; i < (int)pDom->nLeaves; i++ )
    {
        for ( k = 0; k < (int)pCut->nLeaves; k++ )
            if ( pDom->pLeaves[i] == pCut->pLeaves[k] )
                break;
        if ( k == (int)pCut->nLeaves ) // node i in pDom is not contained in pCut
            return 0;
    }
    // every node in pDom is contained in pCut
    return 1;
}

/**Function*************************************************************

  Synopsis    [Check if the cut exists.]

  Description [Returns 1 if the cut exists.]
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
int Lpk_NodeCutsOneFilter( Lpk_Cut_t * pCuts, int nCuts, Lpk_Cut_t * pCutNew )
{
    Lpk_Cut_t * pCut;
    int i, k;
    assert( pCutNew->uSign[0] || pCutNew->uSign[1] );
    // try to find the cut
    for ( i = 0; i < nCuts; i++ )
    {
        pCut = pCuts + i;
        if ( pCut->nLeaves == 0 )
            continue;
        if ( pCut->nLeaves == pCutNew->nLeaves )
        {
            if ( pCut->uSign[0] == pCutNew->uSign[0] && pCut->uSign[1] == pCutNew->uSign[1] )
            {
                for ( k = 0; k < (int)pCutNew->nLeaves; k++ )
                    if ( pCut->pLeaves[k] != pCutNew->pLeaves[k] )
                        break;
                if ( k == (int)pCutNew->nLeaves )
                    return 1;
            }
            continue;
        }
        if ( pCut->nLeaves < pCutNew->nLeaves )
        {
            // skip the non-contained cuts
            if ( (pCut->uSign[0] & pCutNew->uSign[0]) != pCut->uSign[0] )
                continue;
            if ( (pCut->uSign[1] & pCutNew->uSign[1]) != pCut->uSign[1] )
                continue;
            // check containment seriously
            if ( Lpk_NodeCutsOneDominance( pCut, pCutNew ) )
                return 1;
            continue;
        }
        // check potential containment of other cut

        // skip the non-contained cuts
        if ( (pCut->uSign[0] & pCutNew->uSign[0]) != pCutNew->uSign[0] )
            continue;
        if ( (pCut->uSign[1] & pCutNew->uSign[1]) != pCutNew->uSign[1] )
            continue;
        // check containment seriously
        if ( Lpk_NodeCutsOneDominance( pCutNew, pCut ) )
            pCut->nLeaves = 0; // removed
    }
    return 0;
}

/**Function*************************************************************

  Synopsis    [Prints the given cut.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
void Lpk_NodePrintCut( Lpk_Man_t * p, Lpk_Cut_t * pCut, int fLeavesOnly )
{
    Abc_Obj_t * pObj;
    int i;
    if ( !fLeavesOnly )
        printf( "LEAVES:\n" );
    Lpk_CutForEachLeaf( p->pNtk, pCut, pObj, i )
        printf( "%d,", pObj->Id );
    if ( !fLeavesOnly )
    {
        printf( "\nNODES:\n" );
        Lpk_CutForEachNode( p->pNtk, pCut, pObj, i )
        {
            printf( "%d,", pObj->Id );
            assert( Abc_ObjIsNode(pObj) );
        }
        printf( "\n" );
    }
}

/**Function*************************************************************

  Synopsis    [Set the cut signature.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
void Lpk_NodeCutSignature( Lpk_Cut_t * pCut )
{
    unsigned i;
    pCut->uSign[0] = pCut->uSign[1] = 0;
    for ( i = 0; i < pCut->nLeaves; i++ )
    {
        pCut->uSign[(pCut->pLeaves[i] & 32) > 0] |= (1 << (pCut->pLeaves[i] & 31));
        if ( i != pCut->nLeaves - 1 )
            assert( pCut->pLeaves[i] < pCut->pLeaves[i+1] );
    }
}


/**Function*************************************************************

  Synopsis    [Computes the set of all cuts.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
void Lpk_NodeCutsOne( Lpk_Man_t * p, Lpk_Cut_t * pCut, int Node )
{
    Lpk_Cut_t * pCutNew;
    Abc_Obj_t * pObj, * pFanin;
    int i, k, j, nLeavesNew;
/*
    printf( "Exploring cut " );
    Lpk_NodePrintCut( p, pCut, 1 );
    printf( "with node %d\n", Node );
*/
    // check if the cut can stand adding one more internal node
    if ( pCut->nNodes == LPK_SIZE_MAX )
        return;

    // if the node is a PI, quit
    pObj = Abc_NtkObj( p->pNtk, Node );
    if ( Abc_ObjIsCi(pObj) )
        return;
    assert( Abc_ObjIsNode(pObj) );
//    assert( Abc_ObjFaninNum(pObj) <= p->pPars->nLutSize );

    // if the node is not in the MFFC, check the limit
    if ( !Abc_NodeIsTravIdCurrent(pObj) )
    {
        if ( (int)pCut->nNodesDup == p->pPars->nLutsOver )
            return;
        assert( (int)pCut->nNodesDup < p->pPars->nLutsOver );
    }

    // check the possibility of adding this node using the signature
    nLeavesNew = pCut->nLeaves - 1;
    Abc_ObjForEachFanin( pObj, pFanin, i )
    {
        if ( (pCut->uSign[(pFanin->Id & 32) > 0] & (1 << (pFanin->Id & 31))) )
            continue;
        if ( ++nLeavesNew > p->pPars->nVarsMax )
            return;
    }

    // initialize the set of leaves to the nodes in the cut
    assert( p->nCuts < LPK_CUTS_MAX );
    pCutNew = p->pCuts + p->nCuts;
    pCutNew->nLeaves = 0;
    for ( i = 0; i < (int)pCut->nLeaves; i++ )
        if ( pCut->pLeaves[i] != Node )
            pCutNew->pLeaves[pCutNew->nLeaves++] = pCut->pLeaves[i];

    // add new nodes
    Abc_ObjForEachFanin( pObj, pFanin, i )
    {
        // find the place where this node belongs
        for ( k = 0; k < (int)pCutNew->nLeaves; k++ )
            if ( pCutNew->pLeaves[k] >= pFanin->Id )
                break;
        if ( k < (int)pCutNew->nLeaves && pCutNew->pLeaves[k] == pFanin->Id )
            continue;
        // check if there is room
        if ( (int)pCutNew->nLeaves == p->pPars->nVarsMax )
            return;
        // move all the nodes
        for ( j = pCutNew->nLeaves; j > k; j-- )
            pCutNew->pLeaves[j] = pCutNew->pLeaves[j-1];
        pCutNew->pLeaves[k] = pFanin->Id;
        pCutNew->nLeaves++;
        assert( pCutNew->nLeaves <= LPK_SIZE_MAX );

    }
    // skip the contained cuts
    Lpk_NodeCutSignature( pCutNew );
    if ( Lpk_NodeCutsOneFilter( p->pCuts, p->nCuts, pCutNew ) )
        return;

    // update the set of internal nodes
    assert( pCut->nNodes < LPK_SIZE_MAX );
    memcpy( pCutNew->pNodes, pCut->pNodes, pCut->nNodes * sizeof(int) );
    pCutNew->nNodes = pCut->nNodes;
    pCutNew->nNodesDup = pCut->nNodesDup;

    // check if the node is already there
    // if so, move the node to be the last
    for ( i = 0; i < (int)pCutNew->nNodes; i++ )
        if ( pCutNew->pNodes[i] == Node )
        {
            for ( k = i; k < (int)pCutNew->nNodes - 1; k++ )
                pCutNew->pNodes[k] = pCutNew->pNodes[k+1];
            pCutNew->pNodes[k] = Node;
            break;
        }
    if ( i == (int)pCutNew->nNodes ) // new node
    {
        pCutNew->pNodes[ pCutNew->nNodes++ ] = Node;
        pCutNew->nNodesDup += !Abc_NodeIsTravIdCurrent(pObj);
    }
    // the number of nodes does not exceed MFFC plus duplications
    assert( pCutNew->nNodes <= p->nMffc + pCutNew->nNodesDup );
    // add the cut to storage
    assert( p->nCuts < LPK_CUTS_MAX );
    p->nCuts++;
}

/**Function*************************************************************

  Synopsis    [Computes the set of all cuts.]

  Description []
               
  SideEffects []

  SeeAlso     []

***********************************************************************/
int Lpk_NodeCuts( Lpk_Man_t * p )
{
    Lpk_Cut_t * pCut, * pCut2;
    int i, k, Temp, nMffc, fChanges;

    // mark the MFFC of the node with the current trav ID
    nMffc = p->nMffc = Abc_NodeMffcLabel( p->pObj );
    assert( nMffc > 0 );
    if ( nMffc == 1 )
        return 0;

    // initialize the first cut
    pCut = p->pCuts; p->nCuts = 1;
    pCut->nNodes = 0; 
    pCut->nNodesDup = 0;
    pCut->nLeaves = 1;
    pCut->pLeaves[0] = p->pObj->Id;
    // assign the signature
    Lpk_NodeCutSignature( pCut );

    // perform the cut computation
    for ( i = 0; i < p->nCuts; i++ )
    {
        pCut = p->pCuts + i;
        if ( pCut->nLeaves == 0 )
            continue;

        // try to expand the fanins of this cut
        for ( k = 0; k < (int)pCut->nLeaves; k++ )
        {
            // create a new cut
            Lpk_NodeCutsOne( p, pCut, pCut->pLeaves[k] );
            // quit if the number of cuts has exceeded the limit
            if ( p->nCuts == LPK_CUTS_MAX )
                break;
        }
        if ( p->nCuts == LPK_CUTS_MAX )
            break;
    }
    if ( p->nCuts == LPK_CUTS_MAX ) 
        p->nNodesOver++;

    // record the impact of this node
    if ( p->pPars->fSatur )
        Lpk_NodeRecordImpact( p );

    // compress the cuts by removing empty ones, those with negative Weight, and decomposable ones
    p->nEvals = 0;
    for ( i = 0; i < p->nCuts; i++ )
    {
        pCut = p->pCuts + i;
        if ( pCut->nLeaves < 2 )
            continue;
        // compute the minimum number of LUTs needed to implement this cut
        // V = N * (K-1) + 1  ~~~~~  N = Ceiling[(V-1)/(K-1)] = (V-1)/(K-1) + [(V-1)%(K-1) > 0]
        pCut->nLuts = Lpk_LutNumLuts( pCut->nLeaves, p->pPars->nLutSize ); 
//        pCut->Weight = (float)1.0 * (pCut->nNodes - pCut->nNodesDup - 1) / pCut->nLuts; //p->pPars->nLutsMax;
        pCut->Weight = (float)1.0 * (pCut->nNodes - pCut->nNodesDup) / pCut->nLuts; //p->pPars->nLutsMax;
        if ( pCut->Weight <= 1.001 )
//        if ( pCut->Weight <= 0.999 )
            continue;
        pCut->fHasDsd = Lpk_NodeCutsCheckDsd( p, pCut );
        if ( pCut->fHasDsd )
            continue;
        p->pEvals[p->nEvals++] = i;
    }
    if ( p->nEvals == 0 )
        return 0;

    // sort the cuts by Weight
    do {
        fChanges = 0;
        for ( i = 0; i < p->nEvals - 1; i++ )
        {
            pCut = p->pCuts + p->pEvals[i];
            pCut2 = p->pCuts + p->pEvals[i+1];
            if ( pCut->Weight >= pCut2->Weight - 0.001 )
                continue;
            Temp = p->pEvals[i];
            p->pEvals[i] = p->pEvals[i+1];
            p->pEvals[i+1] = Temp;
            fChanges = 1;
        }
    } while ( fChanges );
/*
    for ( i = 0; i < p->nEvals; i++ )
    {
        pCut = p->pCuts + p->pEvals[i];
        printf( "Cut %3d : W = %5.2f.\n", i, pCut->Weight );
    }
    printf( "\n" );
*/
    return 1;
}

////////////////////////////////////////////////////////////////////////
///                       END OF FILE                                ///
////////////////////////////////////////////////////////////////////////


ABC_NAMESPACE_IMPL_END

