1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18package android.filterfw.core;
19
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.Iterator;
23import java.util.LinkedList;
24import java.util.Map.Entry;
25import java.util.Set;
26import java.util.Stack;
27
28import android.filterfw.core.FilterContext;
29import android.filterfw.core.KeyValueMap;
30import android.filterpacks.base.FrameBranch;
31import android.filterpacks.base.NullFilter;
32
33import android.util.Log;
34
35/**
36 * @hide
37 */
38public class FilterGraph {
39
40    private HashSet<Filter> mFilters = new HashSet<Filter>();
41    private HashMap<String, Filter> mNameMap = new HashMap<String, Filter>();
42    private HashMap<OutputPort, LinkedList<InputPort>> mPreconnections = new
43            HashMap<OutputPort, LinkedList<InputPort>>();
44
45    public static final int AUTOBRANCH_OFF      = 0;
46    public static final int AUTOBRANCH_SYNCED   = 1;
47    public static final int AUTOBRANCH_UNSYNCED = 2;
48
49    public static final int TYPECHECK_OFF       = 0;
50    public static final int TYPECHECK_DYNAMIC   = 1;
51    public static final int TYPECHECK_STRICT    = 2;
52
53    private boolean mIsReady = false;
54    private int mAutoBranchMode = AUTOBRANCH_OFF;
55    private int mTypeCheckMode = TYPECHECK_STRICT;
56    private boolean mDiscardUnconnectedOutputs = false;
57
58    private boolean mLogVerbose;
59    private String TAG = "FilterGraph";
60
61    public FilterGraph() {
62        mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
63    }
64
65    public boolean addFilter(Filter filter) {
66        if (!containsFilter(filter)) {
67            mFilters.add(filter);
68            mNameMap.put(filter.getName(), filter);
69            return true;
70        }
71        return false;
72    }
73
74    public boolean containsFilter(Filter filter) {
75        return mFilters.contains(filter);
76    }
77
78    public Filter getFilter(String name) {
79        return mNameMap.get(name);
80    }
81
82    public void connect(Filter source,
83                        String outputName,
84                        Filter target,
85                        String inputName) {
86        if (source == null || target == null) {
87            throw new IllegalArgumentException("Passing null Filter in connect()!");
88        } else if (!containsFilter(source) || !containsFilter(target)) {
89            throw new RuntimeException("Attempting to connect filter not in graph!");
90        }
91
92        OutputPort outPort = source.getOutputPort(outputName);
93        InputPort inPort = target.getInputPort(inputName);
94        if (outPort == null) {
95            throw new RuntimeException("Unknown output port '" + outputName + "' on Filter " +
96                                       source + "!");
97        } else if (inPort == null) {
98            throw new RuntimeException("Unknown input port '" + inputName + "' on Filter " +
99                                       target + "!");
100        }
101
102        preconnect(outPort, inPort);
103    }
104
105    public void connect(String sourceName,
106                        String outputName,
107                        String targetName,
108                        String inputName) {
109        Filter source = getFilter(sourceName);
110        Filter target = getFilter(targetName);
111        if (source == null) {
112            throw new RuntimeException(
113                "Attempting to connect unknown source filter '" + sourceName + "'!");
114        } else if (target == null) {
115            throw new RuntimeException(
116                "Attempting to connect unknown target filter '" + targetName + "'!");
117        }
118        connect(source, outputName, target, inputName);
119    }
120
121    public Set<Filter> getFilters() {
122        return mFilters;
123    }
124
125    public void beginProcessing() {
126        if (mLogVerbose) Log.v(TAG, "Opening all filter connections...");
127        for (Filter filter : mFilters) {
128            filter.openOutputs();
129        }
130        mIsReady = true;
131    }
132
133    public void flushFrames() {
134        for (Filter filter : mFilters) {
135            filter.clearOutputs();
136        }
137    }
138
139    public void closeFilters(FilterContext context) {
140        if (mLogVerbose) Log.v(TAG, "Closing all filters...");
141        for (Filter filter : mFilters) {
142            filter.performClose(context);
143        }
144        mIsReady = false;
145    }
146
147    public boolean isReady() {
148        return mIsReady;
149    }
150
151    public void setAutoBranchMode(int autoBranchMode) {
152        mAutoBranchMode = autoBranchMode;
153    }
154
155    public void setDiscardUnconnectedOutputs(boolean discard) {
156        mDiscardUnconnectedOutputs = discard;
157    }
158
159    public void setTypeCheckMode(int typeCheckMode) {
160        mTypeCheckMode = typeCheckMode;
161    }
162
163    public void tearDown(FilterContext context) {
164        if (!mFilters.isEmpty()) {
165            flushFrames();
166            for (Filter filter : mFilters) {
167                filter.performTearDown(context);
168            }
169            mFilters.clear();
170            mNameMap.clear();
171            mIsReady = false;
172        }
173    }
174
175    private boolean readyForProcessing(Filter filter, Set<Filter> processed) {
176        // Check if this has been already processed
177        if (processed.contains(filter)) {
178            return false;
179        }
180
181        // Check if all dependencies have been processed
182        for (InputPort port : filter.getInputPorts()) {
183            Filter dependency = port.getSourceFilter();
184            if (dependency != null && !processed.contains(dependency)) {
185                return false;
186            }
187        }
188        return true;
189    }
190
191    private void runTypeCheck() {
192        Stack<Filter> filterStack = new Stack<Filter>();
193        Set<Filter> processedFilters = new HashSet<Filter>();
194        filterStack.addAll(getSourceFilters());
195
196        while (!filterStack.empty()) {
197            // Get current filter and mark as processed
198            Filter filter = filterStack.pop();
199            processedFilters.add(filter);
200
201            // Anchor output formats
202            updateOutputs(filter);
203
204            // Perform type check
205            if (mLogVerbose) Log.v(TAG, "Running type check on " + filter + "...");
206            runTypeCheckOn(filter);
207
208            // Push connected filters onto stack
209            for (OutputPort port : filter.getOutputPorts()) {
210                Filter target = port.getTargetFilter();
211                if (target != null && readyForProcessing(target, processedFilters)) {
212                    filterStack.push(target);
213                }
214            }
215        }
216
217        // Make sure all ports were setup
218        if (processedFilters.size() != getFilters().size()) {
219            throw new RuntimeException("Could not schedule all filters! Is your graph malformed?");
220        }
221    }
222
223    private void updateOutputs(Filter filter) {
224        for (OutputPort outputPort : filter.getOutputPorts()) {
225            InputPort inputPort = outputPort.getBasePort();
226            if (inputPort != null) {
227                FrameFormat inputFormat = inputPort.getSourceFormat();
228                FrameFormat outputFormat = filter.getOutputFormat(outputPort.getName(),
229                                                                  inputFormat);
230                if (outputFormat == null) {
231                    throw new RuntimeException("Filter did not return an output format for "
232                        + outputPort + "!");
233                }
234                outputPort.setPortFormat(outputFormat);
235            }
236        }
237    }
238
239    private void runTypeCheckOn(Filter filter) {
240        for (InputPort inputPort : filter.getInputPorts()) {
241            if (mLogVerbose) Log.v(TAG, "Type checking port " + inputPort);
242            FrameFormat sourceFormat = inputPort.getSourceFormat();
243            FrameFormat targetFormat = inputPort.getPortFormat();
244            if (sourceFormat != null && targetFormat != null) {
245                if (mLogVerbose) Log.v(TAG, "Checking " + sourceFormat + " against " + targetFormat + ".");
246
247                boolean compatible = true;
248                switch (mTypeCheckMode) {
249                    case TYPECHECK_OFF:
250                        inputPort.setChecksType(false);
251                        break;
252                    case TYPECHECK_DYNAMIC:
253                        compatible = sourceFormat.mayBeCompatibleWith(targetFormat);
254                        inputPort.setChecksType(true);
255                        break;
256                    case TYPECHECK_STRICT:
257                        compatible = sourceFormat.isCompatibleWith(targetFormat);
258                        inputPort.setChecksType(false);
259                        break;
260                }
261
262                if (!compatible) {
263                    throw new RuntimeException("Type mismatch: Filter " + filter + " expects a "
264                        + "format of type " + targetFormat + " but got a format of type "
265                        + sourceFormat + "!");
266                }
267            }
268        }
269    }
270
271    private void checkConnections() {
272        // TODO
273    }
274
275    private void discardUnconnectedOutputs() {
276        // Connect unconnected ports to Null filters
277        LinkedList<Filter> addedFilters = new LinkedList<Filter>();
278        for (Filter filter : mFilters) {
279            int id = 0;
280            for (OutputPort port : filter.getOutputPorts()) {
281                if (!port.isConnected()) {
282                    if (mLogVerbose) Log.v(TAG, "Autoconnecting unconnected " + port + " to Null filter.");
283                    NullFilter nullFilter = new NullFilter(filter.getName() + "ToNull" + id);
284                    nullFilter.init();
285                    addedFilters.add(nullFilter);
286                    port.connectTo(nullFilter.getInputPort("frame"));
287                    ++id;
288                }
289            }
290        }
291        // Add all added filters to this graph
292        for (Filter filter : addedFilters) {
293            addFilter(filter);
294        }
295    }
296
297    private void removeFilter(Filter filter) {
298        mFilters.remove(filter);
299        mNameMap.remove(filter.getName());
300    }
301
302    private void preconnect(OutputPort outPort, InputPort inPort) {
303        LinkedList<InputPort> targets;
304        targets = mPreconnections.get(outPort);
305        if (targets == null) {
306            targets = new LinkedList<InputPort>();
307            mPreconnections.put(outPort, targets);
308        }
309        targets.add(inPort);
310    }
311
312    private void connectPorts() {
313        int branchId = 1;
314        for (Entry<OutputPort, LinkedList<InputPort>> connection : mPreconnections.entrySet()) {
315            OutputPort outputPort = connection.getKey();
316            LinkedList<InputPort> inputPorts = connection.getValue();
317            if (inputPorts.size() == 1) {
318                outputPort.connectTo(inputPorts.get(0));
319            } else if (mAutoBranchMode == AUTOBRANCH_OFF) {
320                throw new RuntimeException("Attempting to connect " + outputPort + " to multiple "
321                                         + "filter ports! Enable auto-branching to allow this.");
322            } else {
323                if (mLogVerbose) Log.v(TAG, "Creating branch for " + outputPort + "!");
324                FrameBranch branch = null;
325                if (mAutoBranchMode == AUTOBRANCH_SYNCED) {
326                    branch = new FrameBranch("branch" + branchId++);
327                } else {
328                    throw new RuntimeException("TODO: Unsynced branches not implemented yet!");
329                }
330                KeyValueMap branchParams = new KeyValueMap();
331                branch.initWithAssignmentList("outputs", inputPorts.size());
332                addFilter(branch);
333                outputPort.connectTo(branch.getInputPort("in"));
334                Iterator<InputPort> inputPortIter = inputPorts.iterator();
335                for (OutputPort branchOutPort : ((Filter)branch).getOutputPorts()) {
336                    branchOutPort.connectTo(inputPortIter.next());
337                }
338            }
339        }
340        mPreconnections.clear();
341    }
342
343    private HashSet<Filter> getSourceFilters() {
344        HashSet<Filter> sourceFilters = new HashSet<Filter>();
345        for (Filter filter : getFilters()) {
346            if (filter.getNumberOfConnectedInputs() == 0) {
347                if (mLogVerbose) Log.v(TAG, "Found source filter: " + filter);
348                sourceFilters.add(filter);
349            }
350        }
351        return sourceFilters;
352    }
353
354    // Core internal methods /////////////////////////////////////////////////////////////////////////
355    void setupFilters() {
356        if (mDiscardUnconnectedOutputs) {
357            discardUnconnectedOutputs();
358        }
359        connectPorts();
360        checkConnections();
361        runTypeCheck();
362    }
363}
364